~citrix-openstack/nova/xenapi

« back to all changes in this revision

Viewing changes to nova/volume/driver.py

  • Committer: rlane at wikimedia
  • Date: 2011-03-03 23:04:11 UTC
  • mfrom: (408.9.363 nova)
  • mto: (408.9.366 nova)
  • mto: This revision was merged to the branch mainline in revision 449.
  • Revision ID: rlane@wikimedia.org-20110303230411-qx4qndimfyr1w1fx
MergeĀ fromĀ trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
"""
22
22
 
23
23
import time
 
24
import os
24
25
 
25
26
from nova import exception
26
27
from nova import flags
36
37
                    'Which device to export the volumes on')
37
38
flags.DEFINE_string('num_shell_tries', 3,
38
39
                    'number of times to attempt to run flakey shell commands')
 
40
flags.DEFINE_string('num_iscsi_scan_tries', 3,
 
41
                    'number of times to rescan iSCSI target to find volume')
39
42
flags.DEFINE_integer('num_shelves',
40
43
                    100,
41
44
                    'Number of vblade shelves')
62
65
        self._execute = execute
63
66
        self._sync_exec = sync_exec
64
67
 
65
 
    def _try_execute(self, command):
 
68
    def _try_execute(self, *command):
66
69
        # NOTE(vish): Volume commands can partially fail due to timing, but
67
70
        #             running them a second time on failure will usually
68
71
        #             recover nicely.
69
72
        tries = 0
70
73
        while True:
71
74
            try:
72
 
                self._execute(command)
 
75
                self._execute(*command)
73
76
                return True
74
77
            except exception.ProcessExecutionError:
75
78
                tries = tries + 1
81
84
 
82
85
    def check_for_setup_error(self):
83
86
        """Returns an error if prerequisites aren't met"""
84
 
        out, err = self._execute("sudo vgs --noheadings -o name")
 
87
        out, err = self._execute('sudo', 'vgs', '--noheadings', '-o', 'name')
85
88
        volume_groups = out.split()
86
89
        if not FLAGS.volume_group in volume_groups:
87
90
            raise exception.Error(_("volume group %s doesn't exist")
88
91
                                  % FLAGS.volume_group)
89
92
 
90
93
    def create_volume(self, volume):
91
 
        """Creates a logical volume."""
 
94
        """Creates a logical volume. Can optionally return a Dictionary of
 
95
        changes to the volume object to be persisted."""
92
96
        if int(volume['size']) == 0:
93
97
            sizestr = '100M'
94
98
        else:
95
99
            sizestr = '%sG' % volume['size']
96
 
        self._try_execute("sudo lvcreate -L %s -n %s %s" %
97
 
                          (sizestr,
 
100
        self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n',
98
101
                           volume['name'],
99
 
                           FLAGS.volume_group))
 
102
                           FLAGS.volume_group)
100
103
 
101
104
    def delete_volume(self, volume):
102
105
        """Deletes a logical volume."""
103
106
        try:
104
 
            self._try_execute("sudo lvdisplay %s/%s" %
 
107
            self._try_execute('sudo', 'lvdisplay',
 
108
                              '%s/%s' %
105
109
                              (FLAGS.volume_group,
106
110
                               volume['name']))
107
111
        except Exception as e:
108
112
            # If the volume isn't present, then don't attempt to delete
109
113
            return True
110
114
 
111
 
        self._try_execute("sudo lvremove -f %s/%s" %
 
115
        self._try_execute('sudo', 'lvremove', '-f', "%s/%s" %
112
116
                          (FLAGS.volume_group,
113
117
                           volume['name']))
114
118
 
123
127
        raise NotImplementedError()
124
128
 
125
129
    def create_export(self, context, volume):
126
 
        """Exports the volume."""
 
130
        """Exports the volume. Can optionally return a Dictionary of changes
 
131
        to the volume object to be persisted."""
127
132
        raise NotImplementedError()
128
133
 
129
134
    def remove_export(self, context, volume):
163
168
         blade_id) = self.db.volume_allocate_shelf_and_blade(context,
164
169
                                                             volume['id'])
165
170
        self._try_execute(
166
 
                "sudo vblade-persist setup %s %s %s /dev/%s/%s" %
167
 
                (shelf_id,
 
171
                'sudo', 'vblade-persist', 'setup',
 
172
                 shelf_id,
168
173
                 blade_id,
169
174
                 FLAGS.aoe_eth_dev,
170
 
                 FLAGS.volume_group,
171
 
                 volume['name']))
 
175
                 "/dev/%s/%s" %
 
176
                 (FLAGS.volume_group,
 
177
                  volume['name']))
172
178
        # NOTE(vish): The standard _try_execute does not work here
173
179
        #             because these methods throw errors if other
174
180
        #             volumes on this host are in the process of
177
183
        #             just wait a bit for the current volume to
178
184
        #             be ready and ignore any errors.
179
185
        time.sleep(2)
180
 
        self._execute("sudo vblade-persist auto all",
 
186
        self._execute('sudo', 'vblade-persist', 'auto', 'all',
181
187
                      check_exit_code=False)
182
 
        self._execute("sudo vblade-persist start all",
 
188
        self._execute('sudo', 'vblade-persist', 'start', 'all',
183
189
                      check_exit_code=False)
184
190
 
185
191
    def remove_export(self, context, volume):
187
193
        (shelf_id,
188
194
         blade_id) = self.db.volume_get_shelf_and_blade(context,
189
195
                                                        volume['id'])
190
 
        self._try_execute("sudo vblade-persist stop %s %s" %
191
 
                          (shelf_id, blade_id))
192
 
        self._try_execute("sudo vblade-persist destroy %s %s" %
193
 
                          (shelf_id, blade_id))
 
196
        self._try_execute('sudo', 'vblade-persist', 'stop',
 
197
                          shelf_id, blade_id)
 
198
        self._try_execute('sudo', 'vblade-persist', 'destroy',
 
199
                          shelf_id, blade_id)
194
200
 
195
201
    def discover_volume(self, _volume):
196
202
        """Discover volume on a remote host."""
197
 
        self._execute("sudo aoe-discover")
198
 
        self._execute("sudo aoe-stat", check_exit_code=False)
 
203
        self._execute('sudo', 'aoe-discover')
 
204
        self._execute('sudo', 'aoe-stat', check_exit_code=False)
199
205
 
200
206
    def undiscover_volume(self, _volume):
201
207
        """Undiscover volume on a remote host."""
222
228
 
223
229
 
224
230
class ISCSIDriver(VolumeDriver):
225
 
    """Executes commands relating to ISCSI volumes."""
 
231
    """Executes commands relating to ISCSI volumes.
 
232
 
 
233
    We make use of model provider properties as follows:
 
234
 
 
235
    :provider_location:    if present, contains the iSCSI target information
 
236
                           in the same format as an ietadm discovery
 
237
                           i.e. '<ip>:<port>,<portal> <target IQN>'
 
238
 
 
239
    :provider_auth:    if present, contains a space-separated triple:
 
240
                       '<auth method> <auth username> <auth password>'.
 
241
                       `CHAP` is the only auth_method in use at the moment.
 
242
    """
226
243
 
227
244
    def ensure_export(self, context, volume):
228
245
        """Synchronously recreates an export for a logical volume."""
236
253
 
237
254
        iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
238
255
        volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
239
 
        self._sync_exec("sudo ietadm --op new "
240
 
                        "--tid=%s --params Name=%s" %
241
 
                        (iscsi_target, iscsi_name),
 
256
        self._sync_exec('sudo', 'ietadm', '--op', 'new',
 
257
                        "--tid=%s" % iscsi_target,
 
258
                        '--params',
 
259
                        "Name=%s" % iscsi_name,
242
260
                        check_exit_code=False)
243
 
        self._sync_exec("sudo ietadm --op new --tid=%s "
244
 
                        "--lun=0 --params Path=%s,Type=fileio" %
245
 
                        (iscsi_target, volume_path),
 
261
        self._sync_exec('sudo', 'ietadm', '--op', 'new',
 
262
                        "--tid=%s" % iscsi_target,
 
263
                        '--lun=0',
 
264
                        '--params',
 
265
                        "Path=%s,Type=fileio" % volume_path,
246
266
                        check_exit_code=False)
247
267
 
248
268
    def _ensure_iscsi_targets(self, context, host):
263
283
                                                      volume['host'])
264
284
        iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
265
285
        volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
266
 
        self._execute("sudo ietadm --op new "
267
 
                      "--tid=%s --params Name=%s" %
 
286
        self._execute('sudo', 'ietadm', '--op', 'new',
 
287
                      '--tid=%s --params Name=%s' %
268
288
                      (iscsi_target, iscsi_name))
269
 
        self._execute("sudo ietadm --op new --tid=%s "
270
 
                      "--lun=0 --params Path=%s,Type=fileio" %
271
 
                      (iscsi_target, volume_path))
 
289
        self._execute('sudo', 'ietadm', '--op', 'new',
 
290
                      '--tid=%s' % iscsi_target,
 
291
                      '--lun=0', '--params',
 
292
                      'Path=%s,Type=fileio' % volume_path)
272
293
 
273
294
    def remove_export(self, context, volume):
274
295
        """Removes an export for a logical volume."""
283
304
        try:
284
305
            # ietadm show will exit with an error
285
306
            # this export has already been removed
286
 
            self._execute("sudo ietadm --op show --tid=%s " % iscsi_target)
 
307
            self._execute('sudo', 'ietadm', '--op', 'show',
 
308
                          '--tid=%s' % iscsi_target)
287
309
        except Exception as e:
288
310
            LOG.info(_("Skipping remove_export. No iscsi_target " +
289
311
                       "is presently exported for volume: %d"), volume['id'])
290
312
            return
291
313
 
292
 
        self._execute("sudo ietadm --op delete --tid=%s "
293
 
                      "--lun=0" % iscsi_target)
294
 
        self._execute("sudo ietadm --op delete --tid=%s" %
295
 
                      iscsi_target)
296
 
 
297
 
    def _get_name_and_portal(self, volume_name, host):
298
 
        """Gets iscsi name and portal from volume name and host."""
299
 
        (out, _err) = self._execute("sudo iscsiadm -m discovery -t "
300
 
                                    "sendtargets -p %s" % host)
 
314
        self._execute('sudo', 'ietadm', '--op', 'delete',
 
315
                      '--tid=%s' % iscsi_target,
 
316
                      '--lun=0')
 
317
        self._execute('sudo', 'ietadm', '--op', 'delete',
 
318
                      '--tid=%s' % iscsi_target)
 
319
 
 
320
    def _do_iscsi_discovery(self, volume):
 
321
        #TODO(justinsb): Deprecate discovery and use stored info
 
322
        #NOTE(justinsb): Discovery won't work with CHAP-secured targets (?)
 
323
        LOG.warn(_("ISCSI provider_location not stored, using discovery"))
 
324
 
 
325
        volume_name = volume['name']
 
326
 
 
327
        (out, _err) = self._execute('sudo', 'iscsiadm', '-m', 'discovery',
 
328
                                    '-t', 'sendtargets', '-p', volume['host'])
301
329
        for target in out.splitlines():
302
330
            if FLAGS.iscsi_ip_prefix in target and volume_name in target:
303
 
                (location, _sep, iscsi_name) = target.partition(" ")
304
 
                break
305
 
        iscsi_portal = location.split(",")[0]
306
 
        return (iscsi_name, iscsi_portal)
 
331
                return target
 
332
        return None
 
333
 
 
334
    def _get_iscsi_properties(self, volume):
 
335
        """Gets iscsi configuration
 
336
 
 
337
        We ideally get saved information in the volume entity, but fall back
 
338
        to discovery if need be. Discovery may be completely removed in future
 
339
        The properties are:
 
340
 
 
341
        :target_discovered:    boolean indicating whether discovery was used
 
342
 
 
343
        :target_iqn:    the IQN of the iSCSI target
 
344
 
 
345
        :target_portal:    the portal of the iSCSI target
 
346
 
 
347
        :auth_method:, :auth_username:, :auth_password:
 
348
 
 
349
            the authentication details. Right now, either auth_method is not
 
350
            present meaning no authentication, or auth_method == `CHAP`
 
351
            meaning use CHAP with the specified credentials.
 
352
        """
 
353
 
 
354
        properties = {}
 
355
 
 
356
        location = volume['provider_location']
 
357
 
 
358
        if location:
 
359
            # provider_location is the same format as iSCSI discovery output
 
360
            properties['target_discovered'] = False
 
361
        else:
 
362
            location = self._do_iscsi_discovery(volume)
 
363
 
 
364
            if not location:
 
365
                raise exception.Error(_("Could not find iSCSI export "
 
366
                                        " for volume %s") %
 
367
                                      (volume['name']))
 
368
 
 
369
            LOG.debug(_("ISCSI Discovery: Found %s") % (location))
 
370
            properties['target_discovered'] = True
 
371
 
 
372
        (iscsi_target, _sep, iscsi_name) = location.partition(" ")
 
373
 
 
374
        iscsi_portal = iscsi_target.split(",")[0]
 
375
 
 
376
        properties['target_iqn'] = iscsi_name
 
377
        properties['target_portal'] = iscsi_portal
 
378
 
 
379
        auth = volume['provider_auth']
 
380
 
 
381
        if auth:
 
382
            (auth_method, auth_username, auth_secret) = auth.split()
 
383
 
 
384
            properties['auth_method'] = auth_method
 
385
            properties['auth_username'] = auth_username
 
386
            properties['auth_password'] = auth_secret
 
387
 
 
388
        return properties
 
389
 
 
390
    def _run_iscsiadm(self, iscsi_properties, iscsi_command):
 
391
        command = ("sudo iscsiadm -m node -T %s -p %s %s" %
 
392
                   (iscsi_properties['target_iqn'],
 
393
                    iscsi_properties['target_portal'],
 
394
                    iscsi_command))
 
395
        (out, err) = self._execute(command)
 
396
        LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
 
397
                  (iscsi_command, out, err))
 
398
        return (out, err)
 
399
 
 
400
    def _iscsiadm_update(self, iscsi_properties, property_key, property_value):
 
401
        iscsi_command = ("--op update -n %s -v %s" %
 
402
                         (property_key, property_value))
 
403
        return self._run_iscsiadm(iscsi_properties, iscsi_command)
307
404
 
308
405
    def discover_volume(self, volume):
309
406
        """Discover volume on a remote host."""
310
 
        iscsi_name, iscsi_portal = self._get_name_and_portal(volume['name'],
311
 
                                                             volume['host'])
312
 
        self._execute("sudo iscsiadm -m node -T %s -p %s --login" %
313
 
                      (iscsi_name, iscsi_portal))
314
 
        self._execute("sudo iscsiadm -m node -T %s -p %s --op update "
315
 
                      "-n node.startup -v automatic" %
316
 
                      (iscsi_name, iscsi_portal))
317
 
        return "/dev/disk/by-path/ip-%s-iscsi-%s-lun-0" % (iscsi_portal,
318
 
                                                           iscsi_name)
 
407
        iscsi_properties = self._get_iscsi_properties(volume)
 
408
 
 
409
        if not iscsi_properties['target_discovered']:
 
410
            self._run_iscsiadm(iscsi_properties, "--op new")
 
411
 
 
412
        if iscsi_properties.get('auth_method'):
 
413
            self._iscsiadm_update(iscsi_properties,
 
414
                                  "node.session.auth.authmethod",
 
415
                                  iscsi_properties['auth_method'])
 
416
            self._iscsiadm_update(iscsi_properties,
 
417
                                  "node.session.auth.username",
 
418
                                  iscsi_properties['auth_username'])
 
419
            self._iscsiadm_update(iscsi_properties,
 
420
                                  "node.session.auth.password",
 
421
                                  iscsi_properties['auth_password'])
 
422
 
 
423
        self._run_iscsiadm(iscsi_properties, "--login")
 
424
 
 
425
        self._iscsiadm_update(iscsi_properties, "node.startup", "automatic")
 
426
 
 
427
        mount_device = ("/dev/disk/by-path/ip-%s-iscsi-%s-lun-0" %
 
428
                        (iscsi_properties['target_portal'],
 
429
                         iscsi_properties['target_iqn']))
 
430
 
 
431
        # The /dev/disk/by-path/... node is not always present immediately
 
432
        # TODO(justinsb): This retry-with-delay is a pattern, move to utils?
 
433
        tries = 0
 
434
        while not os.path.exists(mount_device):
 
435
            if tries >= FLAGS.num_iscsi_scan_tries:
 
436
                raise exception.Error(_("iSCSI device not found at %s") %
 
437
                                      (mount_device))
 
438
 
 
439
            LOG.warn(_("ISCSI volume not yet found at: %(mount_device)s. "
 
440
                       "Will rescan & retry.  Try number: %(tries)s") %
 
441
                     locals())
 
442
 
 
443
            # The rescan isn't documented as being necessary(?), but it helps
 
444
            self._run_iscsiadm(iscsi_properties, "--rescan")
 
445
 
 
446
            tries = tries + 1
 
447
            if not os.path.exists(mount_device):
 
448
                time.sleep(tries ** 2)
 
449
 
 
450
        if tries != 0:
 
451
            LOG.debug(_("Found iSCSI node %(mount_device)s "
 
452
                        "(after %(tries)s rescans)") %
 
453
                      locals())
 
454
 
 
455
        return mount_device
319
456
 
320
457
    def undiscover_volume(self, volume):
321
458
        """Undiscover volume on a remote host."""
322
 
        iscsi_name, iscsi_portal = self._get_name_and_portal(volume['name'],
323
 
                                                             volume['host'])
324
 
        self._execute("sudo iscsiadm -m node -T %s -p %s --op update "
325
 
                      "-n node.startup -v manual" %
326
 
                      (iscsi_name, iscsi_portal))
327
 
        self._execute("sudo iscsiadm -m node -T %s -p %s --logout " %
328
 
                      (iscsi_name, iscsi_portal))
329
 
        self._execute("sudo iscsiadm -m node --op delete "
330
 
                      "--targetname %s" % iscsi_name)
 
459
        iscsi_properties = self._get_iscsi_properties(volume)
 
460
        self._iscsiadm_update(iscsi_properties, "node.startup", "manual")
 
461
        self._run_iscsiadm(iscsi_properties, "--logout")
 
462
        self._run_iscsiadm(iscsi_properties, "--op delete")
331
463
 
332
464
 
333
465
class FakeISCSIDriver(ISCSIDriver):
353
485
 
354
486
    def check_for_setup_error(self):
355
487
        """Returns an error if prerequisites aren't met"""
356
 
        (stdout, stderr) = self._execute("rados lspools")
 
488
        (stdout, stderr) = self._execute('rados', 'lspools')
357
489
        pools = stdout.split("\n")
358
490
        if not FLAGS.rbd_pool in pools:
359
491
            raise exception.Error(_("rbd has no pool %s") %
365
497
            size = 100
366
498
        else:
367
499
            size = int(volume['size']) * 1024
368
 
        self._try_execute("rbd --pool %s --size %d create %s" %
369
 
                          (FLAGS.rbd_pool,
370
 
                           size,
371
 
                           volume['name']))
 
500
        self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
 
501
                          '--size', size, 'create', volume['name'])
372
502
 
373
503
    def delete_volume(self, volume):
374
504
        """Deletes a logical volume."""
375
 
        self._try_execute("rbd --pool %s rm %s" %
376
 
                          (FLAGS.rbd_pool,
377
 
                           volume['name']))
 
505
        self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
 
506
                          'rm', voluname['name'])
378
507
 
379
508
    def local_path(self, volume):
380
509
        """Returns the path of the rbd volume."""
409
538
    def check_for_setup_error(self):
410
539
        """Returns an error if prerequisites aren't met"""
411
540
        try:
412
 
            (out, err) = self._execute("collie cluster info")
 
541
            (out, err) = self._execute('collie', 'cluster', 'info')
413
542
            if not out.startswith('running'):
414
543
                raise exception.Error(_("Sheepdog is not working: %s") % out)
415
544
        except exception.ProcessExecutionError:
421
550
            sizestr = '100M'
422
551
        else:
423
552
            sizestr = '%sG' % volume['size']
424
 
        self._try_execute("qemu-img create sheepdog:%s %s" %
425
 
                          (volume['name'], sizestr))
 
553
        self._try_execute('qemu-img', 'create',
 
554
                          "sheepdog:%s" % volume['name'],
 
555
                          sizestr)
426
556
 
427
557
    def delete_volume(self, volume):
428
558
        """Deletes a logical volume"""
429
 
        self._try_execute("collie vdi delete %s" % volume['name'])
 
559
        self._try_execute('collie', 'vdi', 'delete', volume['name'])
430
560
 
431
561
    def local_path(self, volume):
432
562
        return "sheepdog:%s" % volume['name']