Skip to content

Commit

Permalink
Allow nova to guess device if not passed to attach
Browse files Browse the repository at this point in the history
partial fix for bug 1004328

Only the xen hypervisor actually respects the device name that
is passed in attach_volume. For other hypervisors it makes much
more sense to automatically generate a unique name.

This patch generates a non-conflicting device name if one is not
passed to attach_volume. It also validates the passed in volume
name to make sure another device isn't already attached there.

A corresponding change to novaclient and horizon will greatly
improve the user experience of attaching a volume.

It moves some common code out of metadata/base so that it can
be used to get a list of block devices. The code was functionally
tested as well and block device name generation works properly.

This adds a new method to the rpcapi to validate a device name. It
also adds server_id to the volumes extension, since it was omitted
by mistake.

The next step is to modify the libvirt driver to match the serial
number of the device to the volume uuid so that the volume can
always be found at /dev/disk/by-id/virtio-<uuid>.

DocImpact

Change-Id: I0b9454fc50a5c93b4aea38545dcee98f68d7e511
  • Loading branch information
vishvananda committed Aug 15, 2012
1 parent d141e64 commit e447511
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 65 deletions.
51 changes: 2 additions & 49 deletions nova/api/metadata/base.py
Expand Up @@ -45,11 +45,6 @@
FLAGS.register_opts(metadata_opts)


_DEFAULT_MAPPINGS = {'ami': 'sda1',
'ephemeral0': 'sda2',
'root': block_device.DEFAULT_ROOT_DEV_NAME,
'swap': 'sda3'}

VERSIONS = [
'1.0',
'2007-01-19',
Expand Down Expand Up @@ -387,50 +382,8 @@ def get_metadata_by_address(address):


def _format_instance_mapping(ctxt, instance):
root_device_name = instance['root_device_name']
if root_device_name is None:
return _DEFAULT_MAPPINGS

mappings = {}
mappings['ami'] = block_device.strip_dev(root_device_name)
mappings['root'] = root_device_name
default_ephemeral_device = instance.get('default_ephemeral_device')
if default_ephemeral_device:
mappings['ephemeral0'] = default_ephemeral_device
default_swap_device = instance.get('default_swap_device')
if default_swap_device:
mappings['swap'] = default_swap_device
ebs_devices = []

# 'ephemeralN', 'swap' and ebs
for bdm in db.block_device_mapping_get_all_by_instance(
ctxt, instance['uuid']):
if bdm['no_device']:
continue

# ebs volume case
if (bdm['volume_id'] or bdm['snapshot_id']):
ebs_devices.append(bdm['device_name'])
continue

virtual_name = bdm['virtual_name']
if not virtual_name:
continue

if block_device.is_swap_or_ephemeral(virtual_name):
mappings[virtual_name] = bdm['device_name']

# NOTE(yamahata): I'm not sure how ebs device should be numbered.
# Right now sort by device name for deterministic
# result.
if ebs_devices:
nebs = 0
ebs_devices.sort()
for ebs in ebs_devices:
mappings['ebs%d' % nebs] = ebs
nebs += 1

return mappings
bdms = db.block_device_mapping_get_all_by_instance(ctxt, instance['uuid'])
return block_device.instance_block_mapping(instance, bdms)


def ec2_md_print(data):
Expand Down
8 changes: 5 additions & 3 deletions nova/api/openstack/compute/contrib/volumes.py
Expand Up @@ -339,23 +339,25 @@ def create(self, req, server_id, body):
raise exc.HTTPUnprocessableEntity()

volume_id = body['volumeAttachment']['volumeId']
device = body['volumeAttachment']['device']
device = body['volumeAttachment'].get('device')

msg = _("Attach volume %(volume_id)s to instance %(server_id)s"
" at %(device)s") % locals()
LOG.audit(msg, context=context)

try:
instance = self.compute_api.get(context, server_id)
self.compute_api.attach_volume(context, instance,
volume_id, device)
device = self.compute_api.attach_volume(context, instance,
volume_id, device)
except exception.NotFound:
raise exc.HTTPNotFound()

# The attach is async
attachment = {}
attachment['id'] = volume_id
attachment['serverId'] = server_id
attachment['volumeId'] = volume_id
attachment['device'] = device

# NOTE(justinsb): And now, we have a problem...
# The attach is async, so there's a window in which we don't see
Expand Down
50 changes: 50 additions & 0 deletions nova/block_device.py
Expand Up @@ -19,6 +19,10 @@


DEFAULT_ROOT_DEV_NAME = '/dev/sda1'
_DEFAULT_MAPPINGS = {'ami': 'sda1',
'ephemeral0': 'sda2',
'root': DEFAULT_ROOT_DEV_NAME,
'swap': 'sda3'}


def properties_root_device_name(properties):
Expand Down Expand Up @@ -81,3 +85,49 @@ def strip_prefix(device_name):
""" remove both leading /dev/ and xvd or sd or vd """
device_name = strip_dev(device_name)
return _pref.sub('', device_name)


def instance_block_mapping(instance, bdms):
root_device_name = instance['root_device_name']
if root_device_name is None:
return _DEFAULT_MAPPINGS

mappings = {}
mappings['ami'] = strip_dev(root_device_name)
mappings['root'] = root_device_name
default_ephemeral_device = instance.get('default_ephemeral_device')
if default_ephemeral_device:
mappings['ephemeral0'] = default_ephemeral_device
default_swap_device = instance.get('default_swap_device')
if default_swap_device:
mappings['swap'] = default_swap_device
ebs_devices = []

# 'ephemeralN', 'swap' and ebs
for bdm in bdms:
if bdm['no_device']:
continue

# ebs volume case
if (bdm['volume_id'] or bdm['snapshot_id']):
ebs_devices.append(bdm['device_name'])
continue

virtual_name = bdm['virtual_name']
if not virtual_name:
continue

if is_swap_or_ephemeral(virtual_name):
mappings[virtual_name] = bdm['device_name']

# NOTE(yamahata): I'm not sure how ebs device should be numbered.
# Right now sort by device name for deterministic
# result.
if ebs_devices:
nebs = 0
ebs_devices.sort()
for ebs in ebs_devices:
mappings['ebs%d' % nebs] = ebs
nebs += 1

return mappings
32 changes: 25 additions & 7 deletions nova/compute/api.py
Expand Up @@ -1739,15 +1739,33 @@ def inject_network_info(self, context, instance):

@wrap_check_policy
@check_instance_lock
def attach_volume(self, context, instance, volume_id, device):
def attach_volume(self, context, instance, volume_id, device=None):
"""Attach an existing volume to an existing instance."""
if not re.match("^/dev/x{0,1}[a-z]d[a-z]+$", device):
# NOTE(vish): Fail fast if the device is not going to pass. This
# will need to be removed along with the test if we
# change the logic in the manager for what constitutes
# a valid device.
if device and not re.match("^/dev/x{0,1}[a-z]d[a-z]+$", device):
raise exception.InvalidDevicePath(path=device)
volume = self.volume_api.get(context, volume_id)
self.volume_api.check_attach(context, volume)
self.volume_api.reserve_volume(context, volume)
self.compute_rpcapi.attach_volume(context, instance=instance,
volume_id=volume_id, mountpoint=device)
# NOTE(vish): This is done on the compute host because we want
# to avoid a race where two devices are requested at
# the same time. When db access is removed from
# compute, the bdm will be created here and we will
# have to make sure that they are assigned atomically.
device = self.compute_rpcapi.reserve_block_device_name(
context, device=device, instance=instance)
try:
volume = self.volume_api.get(context, volume_id)
self.volume_api.check_attach(context, volume)
self.volume_api.reserve_volume(context, volume)
self.compute_rpcapi.attach_volume(context, instance=instance,
volume_id=volume_id, mountpoint=device)
except Exception:
with excutils.save_and_reraise_exception():
self.db.block_device_mapping_destroy_by_instance_and_device(
context, instance['uuid'], device)

return device

@check_instance_lock
def _detach_volume(self, context, instance, volume_id):
Expand Down
32 changes: 30 additions & 2 deletions nova/compute/manager.py
Expand Up @@ -247,7 +247,7 @@ def _get_image_meta(context, image_ref):
class ComputeManager(manager.SchedulerDependentManager):
"""Manages the running instances from creation to destruction."""

RPC_API_VERSION = '1.43'
RPC_API_VERSION = '1.44'

def __init__(self, compute_driver=None, *args, **kwargs):
"""Load configuration options and connect to the hypervisor."""
Expand Down Expand Up @@ -2023,13 +2023,41 @@ def _attach_volume_boot(self, context, instance, volume, mountpoint):
self.volume_api.attach(context, volume, instance_uuid, mountpoint)
return connection_info

@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@reverts_task_state
@wrap_instance_fault
def reserve_block_device_name(self, context, instance, device):

@utils.synchronized(instance['uuid'])
def do_reserve():
result = compute_utils.get_device_name_for_instance(context,
instance,
device)
# NOTE(vish): create bdm here to avoid race condition
values = {'instance_uuid': instance['uuid'],
'device_name': result}
self.db.block_device_mapping_create(context, values)
return result
return do_reserve()

@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
@reverts_task_state
@checks_instance_lock
@wrap_instance_fault
def attach_volume(self, context, volume_id, mountpoint, instance_uuid=None,
instance=None):
"""Attach a volume to an instance."""
try:
return self._attach_volume(context, volume_id, mountpoint,
instance_uuid, instance)
except Exception:
with excutils.save_and_reraise_exception():
instance_uuid = instance_uuid or instance.get('uuid')
self.db.block_device_mapping_destroy_by_instance_and_device(
context, instance_uuid, mountpoint)

def _attach_volume(self, context, volume_id, mountpoint, instance_uuid,
instance):
volume = self.volume_api.get(context, volume_id)
context = context.elevated()
if not instance:
Expand Down Expand Up @@ -2076,7 +2104,7 @@ def attach_volume(self, context, volume_id, mountpoint, instance_uuid=None,
'volume_id': volume_id,
'volume_size': None,
'no_device': None}
self.db.block_device_mapping_create(context, values)
self.db.block_device_mapping_update_or_create(context, values)

def _detach_volume(self, context, instance, bdm):
"""Do the actual driver detach using block device mapping."""
Expand Down
8 changes: 8 additions & 0 deletions nova/compute/rpcapi.py
Expand Up @@ -124,6 +124,7 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy):
finish_resize(), confirm_resize(), revert_resize() and
finish_revert_resize()
1.43 - Add migrate_data to live_migration()
1.44 - Adds reserve_block_device_name()
'''

BASE_RPC_API_VERSION = '1.0'
Expand Down Expand Up @@ -479,6 +480,13 @@ def get_host_uptime(self, ctxt, host):
return self.call(ctxt, self.make_msg('get_host_uptime'), topic,
version='1.1')

def reserve_block_device_name(self, ctxt, instance, device):
instance_p = jsonutils.to_primitive(instance)
return self.call(ctxt, self.make_msg('reserve_block_device_name',
instance=instance_p, device=device),
topic=_compute_topic(self.topic, ctxt, None, instance),
version='1.44')

def snapshot_instance(self, ctxt, instance, image_id, image_type,
backup_type, rotation):
instance_p = jsonutils.to_primitive(instance)
Expand Down
52 changes: 52 additions & 0 deletions nova/compute/utils.py
Expand Up @@ -16,6 +16,10 @@

"""Compute-related Utilities and helpers."""

import re
import string

from nova import block_device
from nova import db
from nova import exception
from nova import flags
Expand All @@ -29,6 +33,54 @@
LOG = log.getLogger(__name__)


def get_device_name_for_instance(context, instance, device):
# NOTE(vish): this will generate a unique device name that is not
# in use already. It is a reasonable guess at where
# it will show up in a linux guest, but it may not
# always be correct
req_prefix = None
req_letters = None
if device:
try:
match = re.match("(^/dev/x{0,1}[a-z]d)([a-z]+)$", device)
req_prefix, req_letters = match.groups()
except (TypeError, AttributeError, ValueError):
raise exception.InvalidDevicePath(path=device)
bdms = db.block_device_mapping_get_all_by_instance(context,
instance['uuid'])
mappings = block_device.instance_block_mapping(instance, bdms)
try:
match = re.match("(^/dev/x{0,1}[a-z]d)[a-z]+[0-9]*$", mappings['root'])
prefix = match.groups()[0]
except (TypeError, AttributeError, ValueError):
raise exception.InvalidDevicePath(path=mappings['root'])
if not req_prefix:
req_prefix = prefix
letters_list = []
for _name, device in mappings.iteritems():
letter = block_device.strip_prefix(device)
# NOTE(vish): delete numbers in case we have something like
# /dev/sda1
letter = re.sub("\d+", "", letter)
letters_list.append(letter)
used_letters = set(letters_list)
if not req_letters:
req_letters = _get_unused_letters(used_letters)
if req_letters in used_letters:
raise exception.DevicePathInUse(path=device)
return req_prefix + req_letters


def _get_unused_letters(used_letters):
doubles = [first + second for second in string.ascii_lowercase
for first in string.ascii_lowercase]
all_letters = set(list(string.ascii_lowercase) + doubles)
letters = list(all_letters - used_letters)
# NOTE(vish): prepend ` so all shorter sequences sort first
letters.sort(key=lambda x: x.rjust(2, '`'))
return letters[0]


def notify_usage_exists(context, instance_ref, current_period=False,
ignore_missing_network_data=True,
system_metadata=None, extra_usage_info=None):
Expand Down
9 changes: 8 additions & 1 deletion nova/db/api.py
Expand Up @@ -1298,9 +1298,16 @@ def block_device_mapping_destroy(context, bdm_id):
return IMPL.block_device_mapping_destroy(context, bdm_id)


def block_device_mapping_destroy_by_instance_and_device(context, instance_uuid,
device_name):
"""Destroy the block device mapping."""
return IMPL.block_device_mapping_destroy_by_instance_and_device(
context, instance_uuid, device_name)


def block_device_mapping_destroy_by_instance_and_volume(context, instance_uuid,
volume_id):
"""Destroy the block device mapping or raise if it does not exist."""
"""Destroy the block device mapping."""
return IMPL.block_device_mapping_destroy_by_instance_and_volume(
context, instance_uuid, volume_id)

Expand Down
16 changes: 14 additions & 2 deletions nova/db/sqlalchemy/api.py
Expand Up @@ -3463,8 +3463,7 @@ def snapshot_update(context, snapshot_id, values):


def _block_device_mapping_get_query(context, session=None):
return model_query(context, models.BlockDeviceMapping, session=session,
read_deleted="no")
return model_query(context, models.BlockDeviceMapping, session=session)


@require_context
Expand Down Expand Up @@ -3547,6 +3546,19 @@ def block_device_mapping_destroy_by_instance_and_volume(context, instance_uuid,
'updated_at': literal_column('updated_at')})


@require_context
def block_device_mapping_destroy_by_instance_and_device(context, instance_uuid,
device_name):
session = get_session()
with session.begin():
_block_device_mapping_get_query(context, session=session).\
filter_by(instance_uuid=instance_uuid).\
filter_by(device_name=device_name).\
update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})


###################

def _security_group_get_query(context, session=None, read_deleted=None,
Expand Down

0 comments on commit e447511

Please sign in to comment.