Skip to content

Commit

Permalink
Add an instance-locality filter
Browse files Browse the repository at this point in the history
Having an instance and an attached volume on the same physical host
(i.e. data locality) can be desirable in some configurations, in order
to achieve high-performance disk I/O.

This patch adds an InstanceLocalityFilter filter that allow users to
request creation of volumes 'local' to an existing instance, without
specifying the hypervisor's hostname, and without any knowledge of the
underlying back-ends.

In order to work:
- At least one physical host should run both nova-compute and
  cinder-volume services.
- The Extended Server Attributes extension needs to be active in Nova
  (this is by default), so that the 'OS-EXT-SRV-ATTR:host' property is
  returned when requesting instance info.
- The user making the call needs to have sufficient rights for the
  property to be returned by Nova. This can be achieved either by
  changing Nova's policy.json (the 'extended_server_attributes' option),
  or by setting an account with privileged rights in Cinder conf.

For example:
  Instance 01234567-89ab-cdef is running in a hypervisor on the physical
  host 'my-host'.

  To create a 42 GB volume in a back-end hosted by 'my-host':
    cinder create --hint local_to_instance=01234567-89ab-cdef 42

Note:
  Currently it is not recommended to allow instance migrations for
  hypervisors where this hint will be used. In case of instance
  migration, a previously locally-created volume will not be
  automatically migrated. Also in case of instance migration during the
  volume's scheduling, the result is unpredictable.

DocImpact: New Cinder scheduler filter
Change-Id: Id428fa2132c1afed424443083645787ee3cb0399
  • Loading branch information
adrienverge committed Jan 7, 2015
1 parent ebc819c commit 0269a26
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 51 deletions.
40 changes: 36 additions & 4 deletions cinder/compute/nova.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@
"""


from novaclient import exceptions as nova_exceptions
from novaclient import extension
from novaclient import service_catalog
from novaclient.v1_1 import client as nova_client
from novaclient.v1_1.contrib import assisted_volume_snapshots
from novaclient.v1_1.contrib import list_extensions
from oslo.config import cfg
from requests import exceptions as request_exceptions

from cinder import context as ctx
from cinder.db import base
from cinder import exception
from cinder.openstack.common import log as logging

nova_opts = [
Expand Down Expand Up @@ -60,15 +65,21 @@

LOG = logging.getLogger(__name__)

nova_extensions = (assisted_volume_snapshots,
extension.Extension('list_extensions', list_extensions))

def novaclient(context, admin_endpoint=False, privileged_user=False):

def novaclient(context, admin_endpoint=False, privileged_user=False,
timeout=None):
"""Returns a Nova client
@param admin_endpoint: If True, use the admin endpoint template from
configuration ('nova_endpoint_admin_template' and 'nova_catalog_info')
@param privileged_user: If True, use the account from configuration
(requires 'os_privileged_user_name', 'os_privileged_user_password' and
'os_privileged_user_tenant' to be set)
@param timeout: Number of seconds to wait for an answer before raising a
Timeout exception (None to disable)
"""
# FIXME: the novaclient ServiceCatalog object is mis-named.
# It actually contains the entire access blob.
Expand Down Expand Up @@ -119,15 +130,14 @@ def novaclient(context, admin_endpoint=False, privileged_user=False):

LOG.debug('Nova client connection created using URL: %s' % url)

extensions = [assisted_volume_snapshots]

c = nova_client.Client(context.user_id,
context.auth_token,
context.project_name,
auth_url=url,
insecure=CONF.nova_api_insecure,
timeout=timeout,
cacert=CONF.nova_ca_certificates_file,
extensions=extensions)
extensions=nova_extensions)

if not privileged_user:
# noauth extracts user_id:project_id from auth_token
Expand All @@ -140,6 +150,18 @@ def novaclient(context, admin_endpoint=False, privileged_user=False):
class API(base.Base):
"""API for interacting with novaclient."""

def has_extension(self, context, extension, timeout=None):
try:
client = novaclient(context, timeout=timeout)

# Pylint gives a false positive here because the 'list_extensions'
# method is not explicitly declared. Overriding the error.
# pylint: disable-msg=E1101
nova_exts = client.list_extensions.show_all()
except request_exceptions.Timeout:
raise exception.APITimeout(service='Nova')
return extension in [e.name for e in nova_exts]

def update_server_volume(self, context, server_id, attachment_id,
new_volume_id):
novaclient(context).volumes.update_server_volume(server_id,
Expand All @@ -159,3 +181,13 @@ def delete_volume_snapshot(self, context, snapshot_id, delete_info):
nova.assisted_volume_snapshots.delete(
snapshot_id,
delete_info=delete_info)

def get_server(self, context, server_id, privileged_user=False,
timeout=None):
try:
return novaclient(context, privileged_user=privileged_user,
timeout=timeout).servers.get(server_id)
except nova_exceptions.NotFound:
raise exception.ServerNotFound(uuid=server_id)
except request_exceptions.Timeout:
raise exception.APITimeout(service='Nova')
17 changes: 17 additions & 0 deletions cinder/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,19 @@ class InvalidUUID(Invalid):
message = _("Expected a uuid but received %(uuid)s.")


class APIException(CinderException):
message = _("Error while requesting %(service)s API.")

def __init__(self, message=None, **kwargs):
if 'service' not in kwargs:
kwargs['service'] = 'unknown'
super(APIException, self).__init__(message, **kwargs)


class APITimeout(APIException):
message = _("Timeout while requesting %(service)s API.")


class NotFound(CinderException):
message = _("Resource could not be found.")
code = 404
Expand Down Expand Up @@ -290,6 +303,10 @@ class SnapshotNotFound(NotFound):
message = _("Snapshot %(snapshot_id)s could not be found.")


class ServerNotFound(NotFound):
message = _("Instance %(uuid)s could not be found.")


class VolumeIsBusy(CinderException):
message = _("deleting volume %(volume_name)s that has snapshot")

Expand Down
118 changes: 118 additions & 0 deletions cinder/scheduler/filters/instance_locality_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Copyright 2014, Adrien Vergé <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from cinder.compute import nova
from cinder import exception
from cinder.i18n import _, _LW
from cinder.openstack.common import log as logging
from cinder.openstack.common.scheduler import filters
from cinder.openstack.common import uuidutils
from cinder.volume import utils as volume_utils


LOG = logging.getLogger(__name__)

HINT_KEYWORD = 'local_to_instance'
INSTANCE_HOST_PROP = 'OS-EXT-SRV-ATTR:host'
REQUESTS_TIMEOUT = 5


class InstanceLocalityFilter(filters.BaseHostFilter):
"""Schedule volume on the same host as a given instance.
This filter enables selection of a storage back-end located on the host
where the instance's hypervisor is running. This provides data locality:
the instance and the volume are located on the same physical machine.
In order to work:
- The Extended Server Attributes extension needs to be active in Nova (this
is by default), so that the 'OS-EXT-SRV-ATTR:host' property is returned
when requesting instance info.
- Either an account with privileged rights for Nova must be configured in
Cinder configuration (see 'os_privileged_user_name'), or the user making
the call needs to have sufficient rights (see
'extended_server_attributes' in Nova policy).
"""

def __init__(self):
# Cache Nova API answers directly into the Filter object.
# Since a BaseHostFilter instance lives only during the volume's
# scheduling, the cache is re-created for every new volume creation.
self._cache = {}
super(InstanceLocalityFilter, self).__init__()

def _nova_has_extended_server_attributes(self, context):
"""Check Extended Server Attributes presence
Find out whether the Extended Server Attributes extension is activated
in Nova or not. Cache the result to query Nova only once.
"""

if not hasattr(self, '_nova_ext_srv_attr'):
self._nova_ext_srv_attr = nova.API().has_extension(
context, 'ExtendedServerAttributes', timeout=REQUESTS_TIMEOUT)

return self._nova_ext_srv_attr

def host_passes(self, host_state, filter_properties):
context = filter_properties['context']
host = volume_utils.extract_host(host_state.host, 'host')

scheduler_hints = filter_properties.get('scheduler_hints') or {}
instance_uuid = scheduler_hints.get(HINT_KEYWORD, None)

# Without 'local_to_instance' hint
if not instance_uuid:
return True

if not uuidutils.is_uuid_like(instance_uuid):
raise exception.InvalidUUID(uuid=instance_uuid)

# TODO(adrienverge): Currently it is not recommended to allow instance
# migrations for hypervisors where this hint will be used. In case of
# instance migration, a previously locally-created volume will not be
# automatically migrated. Also in case of instance migration during the
# volume's scheduling, the result is unpredictable. A future
# enhancement would be to subscribe to Nova migration events (e.g. via
# Ceilometer).

# First, lookup for already-known information in local cache
if instance_uuid in self._cache:
return self._cache[instance_uuid] == host

if not self._nova_has_extended_server_attributes(context):
LOG.warning(_LW('Hint "%s" dropped because '
'ExtendedServerAttributes not active in Nova.'),
HINT_KEYWORD)
raise exception.CinderException(_('Hint "%s" not supported.') %
HINT_KEYWORD)

server = nova.API().get_server(context, instance_uuid,
privileged_user=True,
timeout=REQUESTS_TIMEOUT)

if not hasattr(server, INSTANCE_HOST_PROP):
LOG.warning(_LW('Hint "%s" dropped because Nova did not return '
'enough information. Either Nova policy needs to '
'be changed or a privileged account for Nova '
'should be specified in conf.'), HINT_KEYWORD)
raise exception.CinderException(_('Hint "%s" not supported.') %
HINT_KEYWORD)

self._cache[instance_uuid] = getattr(server, INSTANCE_HOST_PROP)

# Match if given instance is hosted on host
return self._cache[instance_uuid] == host
13 changes: 6 additions & 7 deletions cinder/tests/compute/test_nova.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import contextlib

import mock
from novaclient.v1_1.contrib import assisted_volume_snapshots

from cinder.compute import nova
from cinder import context
Expand Down Expand Up @@ -47,26 +46,26 @@ def test_nova_client_regular(self, p_client):
p_client.assert_called_once_with(
'regularuser', 'token', None,
auth_url='http://novahost:8774/v2/e3f0833dc08b4cea',
insecure=False, cacert=None,
extensions=[assisted_volume_snapshots])
insecure=False, cacert=None, timeout=None,
extensions=nova.nova_extensions)

@mock.patch('novaclient.v1_1.client.Client')
def test_nova_client_admin_endpoint(self, p_client):
nova.novaclient(self.ctx, admin_endpoint=True)
p_client.assert_called_once_with(
'regularuser', 'token', None,
auth_url='http://novaadmhost:4778/v2/e3f0833dc08b4cea',
insecure=False, cacert=None,
extensions=[assisted_volume_snapshots])
insecure=False, cacert=None, timeout=None,
extensions=nova.nova_extensions)

@mock.patch('novaclient.v1_1.client.Client')
def test_nova_client_privileged_user(self, p_client):
nova.novaclient(self.ctx, privileged_user=True)
p_client.assert_called_once_with(
'adminuser', 'strongpassword', None,
auth_url='http://keystonehost:5000/v2.0',
insecure=False, cacert=None,
extensions=[assisted_volume_snapshots])
insecure=False, cacert=None, timeout=None,
extensions=nova.nova_extensions)


class FakeNovaClient(object):
Expand Down
50 changes: 50 additions & 0 deletions cinder/tests/scheduler/fakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from oslo.utils import timeutils

from cinder.openstack.common import uuidutils
from cinder.scheduler import filter_scheduler
from cinder.scheduler import host_manager

Expand Down Expand Up @@ -73,6 +74,55 @@ def __init__(self, host, attribute_dict):
setattr(self, key, val)


class FakeNovaClient(object):
class Server(object):
def __init__(self, host):
self.uuid = uuidutils.generate_uuid()
self.host = host
setattr(self, 'OS-EXT-SRV-ATTR:host', host)

class ServerManager(object):
def __init__(self):
self._servers = []

def create(self, host):
self._servers.append(FakeNovaClient.Server(host))
return self._servers[-1].uuid

def get(self, server_uuid):
for s in self._servers:
if s.uuid == server_uuid:
return s
return None

def list(self, detailed=True, search_opts=None):
matching = list(self._servers)
if search_opts:
for opt, val in search_opts.iteritems():
matching = [m for m in matching
if getattr(m, opt, None) == val]
return matching

class ListExtResource(object):
def __init__(self, ext_name):
self.name = ext_name

class ListExtManager(object):
def __init__(self, ext_srv_attr=True):
self.ext_srv_attr = ext_srv_attr

def show_all(self):
if self.ext_srv_attr:
return [
FakeNovaClient.ListExtResource('ExtendedServerAttributes')]
return []

def __init__(self, ext_srv_attr=True):
self.servers = FakeNovaClient.ServerManager()
self.list_extensions = FakeNovaClient.ListExtManager(
ext_srv_attr=ext_srv_attr)


def mock_host_manager_db_calls(mock_obj, disabled=None):
services = [
dict(id=1, host='host1', topic='volume', disabled=False,
Expand Down
Loading

0 comments on commit 0269a26

Please sign in to comment.