summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-07-26 19:20:24 +0000
committerGerrit Code Review <review@openstack.org>2012-07-26 19:20:24 +0000
commitfd5d3489f93effee45b3e629b8fb1908ca2d8709 (patch)
tree921bf1448037fc739a220e6ca2f23cd43f416177 /nova
parent39a40142254d4dbad8dfe1509d92e7e9fdbb44fb (diff)
parent0c59003b1cc2ce17e3215e6c69b0dc2407f1b38b (diff)
downloadnova-fd5d3489f93effee45b3e629b8fb1908ca2d8709.tar.gz
nova-fd5d3489f93effee45b3e629b8fb1908ca2d8709.tar.xz
nova-fd5d3489f93effee45b3e629b8fb1908ca2d8709.zip
Merge "Refactor glance image service code"
Diffstat (limited to 'nova')
-rw-r--r--nova/exception.py3
-rw-r--r--nova/image/glance.py182
-rw-r--r--nova/tests/api/openstack/fakes.py6
-rw-r--r--nova/tests/image/test_glance.py196
-rw-r--r--nova/tests/test_xenapi.py6
-rw-r--r--nova/virt/xenapi/vm_utils.py7
6 files changed, 287 insertions, 113 deletions
diff --git a/nova/exception.py b/nova/exception.py
index b63406427..981363fa1 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -200,7 +200,8 @@ class VirtualInterfaceMacAddressException(NovaException):
class GlanceConnectionFailed(NovaException):
- message = _("Connection to glance failed") + ": %(reason)s"
+ message = _("Connection to glance host %(host)s:%(port)s failed: "
+ "%(reason)s")
class MelangeConnectionFailed(NovaException):
diff --git a/nova/image/glance.py b/nova/image/glance.py
index 0422d2083..4bebd2c64 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -20,6 +20,7 @@
from __future__ import absolute_import
import copy
+import itertools
import random
import sys
import time
@@ -33,7 +34,6 @@ from nova import flags
from nova.openstack.common import jsonutils
from nova.openstack.common import log as logging
from nova.openstack.common import timeutils
-from nova import utils
LOG = logging.getLogger(__name__)
@@ -68,88 +68,85 @@ def _create_glance_client(context, host, port):
return glance.client.Client(host, port, **params)
-def pick_glance_api_server():
- """Return which Glance API server to use for the request
-
- This method provides a very primitive form of load-balancing suitable for
- testing and sandbox environments. In production, it would be better to use
- one IP and route that to a real load-balancer.
-
- Returns (host, port)
+def get_api_servers():
"""
- host_port = random.choice(FLAGS.glance_api_servers)
- host, port_str = host_port.split(':')
- port = int(port_str)
- return host, port
-
-
-def _get_glance_client(context, image_href):
- """Get the correct glance client and id for the given image_href.
-
- The image_href param can be an href of the form
- http://myglanceserver:9292/images/42, or just an int such as 42. If the
- image_href is an int, then flags are used to create the default
- glance client.
-
- NOTE: Do not use this or glance.client directly, all other code
- should be using GlanceImageService.
-
- :param image_href: image ref/id for an image
- :returns: a tuple of the form (glance_client, image_id)
-
+ Shuffle a list of FLAGS.glance_api_servers and return an iterator
+ that will cycle through the list, looping around to the beginning
+ if necessary.
"""
- glance_host, glance_port = pick_glance_api_server()
+ api_servers = []
+ for api_server in FLAGS.glance_api_servers:
+ host, port_str = api_server.split(':')
+ api_servers.append((host, int(port_str)))
+ random.shuffle(api_servers)
+ return itertools.cycle(api_servers)
+
+
+class GlanceClientWrapper(object):
+ """Glance client wrapper class that implements retries."""
+
+ def __init__(self, context=None, host=None, port=None):
+ if host is not None:
+ self._create_static_client(context, host, port)
+ else:
+ self.client = None
+ self.api_servers = None
+
+ def _create_static_client(self, context, host, port):
+ """Create a client that we'll use for every call."""
+ self.host = host
+ self.port = port
+ self.client = _create_glance_client(context, self.host, self.port)
+
+ def _create_onetime_client(self, context):
+ """Create a client that will be used for one call."""
+ if self.api_servers is None:
+ self.api_servers = get_api_servers()
+ self.host, self.port = self.api_servers.next()
+ return _create_glance_client(context, self.host, self.port)
+
+ def call(self, context, method, *args, **kwargs):
+ """
+ Call a glance client method. If we get a connection error,
+ retry the request according to FLAGS.glance_num_retries.
+ """
- # check if this is an id
- if '/' not in str(image_href):
- glance_client = _create_glance_client(context,
- glance_host,
- glance_port)
- return (glance_client, image_href)
+ retry_excs = (glance_exception.ClientConnectionError,
+ glance_exception.ServiceUnavailable)
- else:
- try:
- (image_id, glance_host, glance_port) = _parse_image_ref(image_href)
- glance_client = _create_glance_client(context,
- glance_host,
- glance_port)
- except ValueError:
- raise exception.InvalidImageRef(image_href=image_href)
+ num_attempts = 1 + FLAGS.glance_num_retries
- return (glance_client, image_id)
+ for attempt in xrange(1, num_attempts + 1):
+ if self.client:
+ client = self.client
+ else:
+ client = self._create_onetime_client(context)
+ try:
+ return getattr(client, method)(*args, **kwargs)
+ except retry_excs as e:
+ host = self.host
+ port = self.port
+ extra = "retrying"
+ error_msg = _("Error contacting glance server "
+ "'%(host)s:%(port)s' for '%(method)s', %(extra)s.")
+ if attempt == num_attempts:
+ extra = 'done trying'
+ LOG.exception(error_msg, locals())
+ raise exception.GlanceConnectionFailed(
+ host=host, port=port, reason=str(e))
+ LOG.exception(error_msg, locals())
+ time.sleep(1)
+ # Not reached
class GlanceImageService(object):
"""Provides storage and retrieval of disk image objects within Glance."""
def __init__(self, client=None):
+ if client is None:
+ client = GlanceClientWrapper()
self._client = client
- def _get_client(self, context):
- # NOTE(sirp): we want to load balance each request across glance
- # servers. Since GlanceImageService is a long-lived object, `client`
- # is made to choose a new server each time via this property.
- if self._client is not None:
- return self._client
- glance_host, glance_port = pick_glance_api_server()
- return _create_glance_client(context, glance_host, glance_port)
-
- def _call_retry(self, context, name, *args, **kwargs):
- """Retry call to glance server if there is a connection error.
- Suitable only for idempotent calls."""
- for i in xrange(FLAGS.glance_num_retries + 1):
- client = self._get_client(context)
- try:
- return getattr(client, name)(*args, **kwargs)
- except glance_exception.ClientConnectionError as e:
- LOG.exception(_('Connection error contacting glance'
- ' server, retrying'))
-
- time.sleep(1)
-
- raise exception.GlanceConnectionFailed(
- reason=_('Maximum attempts reached'))
-
def detail(self, context, **kwargs):
"""Calls out to Glance for a list of detailed image information."""
params = self._extract_query_params(kwargs)
@@ -180,13 +177,12 @@ class GlanceImageService(object):
# NOTE(vish): don't filter out private images
kwargs['filters'].setdefault('is_public', 'none')
- client = self._get_client(context)
- return self._fetch_images(client.get_images_detailed, **kwargs)
+ return self._fetch_images(context, 'get_images_detailed', **kwargs)
- def _fetch_images(self, fetch_func, **kwargs):
+ def _fetch_images(self, context, fetch_method, **kwargs):
"""Paginate through results from glance server"""
try:
- images = fetch_func(**kwargs)
+ images = self._client.call(context, fetch_method, **kwargs)
except Exception:
_reraise_translated_exception()
@@ -212,14 +208,14 @@ class GlanceImageService(object):
# ignore missing limit, just proceed without it
pass
- for image in self._fetch_images(fetch_func, **kwargs):
+ for image in self._fetch_images(context, fetch_method, **kwargs):
yield image
def show(self, context, image_id):
"""Returns a dict with image data for the given opaque image id."""
try:
- image_meta = self._call_retry(context, 'get_image_meta',
- image_id)
+ image_meta = self._client.call(context, 'get_image_meta',
+ image_id)
except Exception:
_reraise_translated_image_exception(image_id)
@@ -232,8 +228,8 @@ class GlanceImageService(object):
def download(self, context, image_id, data):
"""Calls out to Glance for metadata and data and writes data."""
try:
- image_meta, image_chunks = self._call_retry(context, 'get_image',
- image_id)
+ image_meta, image_chunks = self._client.call(context,
+ 'get_image', image_id)
except Exception:
_reraise_translated_image_exception(image_id)
@@ -253,8 +249,8 @@ class GlanceImageService(object):
LOG.debug(_('Metadata after formatting for Glance %s'),
sent_service_image_meta)
- recv_service_image_meta = self._get_client(context).add_image(
- sent_service_image_meta, data)
+ recv_service_image_meta = self._client.call(context,
+ 'add_image', sent_service_image_meta, data)
# Translate Service -> Base
base_image_meta = self._translate_from_glance(recv_service_image_meta)
@@ -271,10 +267,9 @@ class GlanceImageService(object):
# NOTE(vish): show is to check if image is available
self.show(context, image_id)
image_meta = self._translate_to_glance(image_meta)
- client = self._get_client(context)
try:
- image_meta = client.update_image(image_id, image_meta, data,
- features)
+ image_meta = self._client.call(context, 'update_image',
+ image_id, image_meta, data, features)
except Exception:
_reraise_translated_image_exception(image_id)
@@ -289,9 +284,9 @@ class GlanceImageService(object):
"""
# NOTE(vish): show is to check if image is available
- image_meta = self.show(context, image_id)
+ self.show(context, image_id)
try:
- result = self._get_client(context).delete_image(image_id)
+ result = self._client.call(context, 'delete_image', image_id)
except glance_exception.NotFound:
raise exception.ImageNotFound(image_id=image_id)
return result
@@ -480,12 +475,17 @@ def get_remote_image_service(context, image_href):
# standalone image ID
if '/' not in str(image_href):
image_service = get_default_image_service()
- image_id = image_href
- else:
- (glance_client, image_id) = _get_glance_client(context, image_href)
- image_service = GlanceImageService(glance_client)
+ return image_service, image_href
+
+ try:
+ (image_id, glance_host, glance_port) = _parse_image_ref(image_href)
+ glance_client = GlanceClientWrapper(context=context,
+ host=glance_host, port=glance_port)
+ except ValueError:
+ raise exception.InvalidImageRef(image_href=image_href)
- return (image_service, image_id)
+ image_service = GlanceImageService(client=glance_client)
+ return image_service, image_id
def get_default_image_service():
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 20e675a8c..43ca2aec9 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -262,7 +262,11 @@ def stub_out_glance_add_image(stubs, sent_to_glance):
def stub_out_glance(stubs):
def fake_get_remote_image_service():
client = glance_stubs.StubGlanceClient(_make_image_fixtures())
- return nova.image.glance.GlanceImageService(client)
+ client_wrapper = nova.image.glance.GlanceClientWrapper()
+ client_wrapper.host = 'fake_host'
+ client_wrapper.port = 9292
+ client_wrapper.client = client
+ return nova.image.glance.GlanceImageService(client=client_wrapper)
stubs.Set(nova.image.glance,
'get_default_image_service',
fake_get_remote_image_service)
diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py
index d6c9f8606..4616f0949 100644
--- a/nova/tests/image/test_glance.py
+++ b/nova/tests/image/test_glance.py
@@ -17,6 +17,8 @@
import datetime
+import random
+import time
import glance.common.exception as glance_exception
@@ -91,11 +93,23 @@ class TestGlanceImageService(test.TestCase):
def setUp(self):
super(TestGlanceImageService, self).setUp()
fakes.stub_out_compute_api_snapshot(self.stubs)
+
client = glance_stubs.StubGlanceClient()
- self.service = glance.GlanceImageService(client=client)
+ self.service = self._create_image_service(client)
self.context = context.RequestContext('fake', 'fake', auth_token=True)
self.service.delete_all()
+ def _create_image_service(self, client):
+ def _fake_create_glance_client(context, host, port):
+ return client
+
+ self.stubs.Set(glance, '_create_glance_client',
+ _fake_create_glance_client)
+
+ client_wrapper = glance.GlanceClientWrapper(
+ 'fake', 'fake_host', 9292)
+ return glance.GlanceImageService(client=client_wrapper)
+
@staticmethod
def _make_fixture(**kwargs):
fixture = {'name': None,
@@ -430,29 +444,27 @@ class TestGlanceImageService(test.TestCase):
def test_download_with_retries(self):
tries = [0]
- class GlanceBusyException(Exception):
- pass
-
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
"""A client that fails the first time, then succeeds."""
def get_image(self, image_id):
if tries[0] == 0:
tries[0] = 1
- raise GlanceBusyException()
+ raise glance_exception.ClientConnectionError()
else:
return {}, []
client = MyGlanceStubClient()
- service = glance.GlanceImageService(client=client)
+ service = self._create_image_service(client)
image_id = 1 # doesn't matter
writer = NullWriter()
# When retries are disabled, we should get an exception
self.flags(glance_num_retries=0)
- self.assertRaises(GlanceBusyException, service.download, self.context,
- image_id, writer)
+ self.assertRaises(exception.GlanceConnectionFailed,
+ service.download, self.context, image_id, writer)
# Now lets enable retries. No exception should happen now.
+ tries = [0]
self.flags(glance_num_retries=1)
service.download(self.context, image_id, writer)
@@ -463,7 +475,7 @@ class TestGlanceImageService(test.TestCase):
raise glance_exception.Forbidden()
client = MyGlanceStubClient()
- service = glance.GlanceImageService(client=client)
+ service = self._create_image_service(client)
image_id = 1 # doesn't matter
writer = NullWriter()
self.assertRaises(exception.ImageNotAuthorized, service.download,
@@ -472,13 +484,173 @@ class TestGlanceImageService(test.TestCase):
def test_glance_client_image_id(self):
fixture = self._make_fixture(name='test image')
image_id = self.service.create(self.context, fixture)['id']
- client, same_id = glance._get_glance_client(self.context, image_id)
+ (service, same_id) = glance.get_remote_image_service(
+ self.context, image_id)
self.assertEquals(same_id, image_id)
def test_glance_client_image_ref(self):
fixture = self._make_fixture(name='test image')
image_id = self.service.create(self.context, fixture)['id']
image_url = 'http://something-less-likely/%s' % image_id
- client, same_id = glance._get_glance_client(self.context, image_url)
+ (service, same_id) = glance.get_remote_image_service(
+ self.context, image_url)
self.assertEquals(same_id, image_id)
- self.assertEquals(client.host, 'something-less-likely')
+ self.assertEquals(service._client.host,
+ 'something-less-likely')
+
+
+def _create_failing_glance_client(info):
+ class MyGlanceStubClient(glance_stubs.StubGlanceClient):
+ """A client that fails the first time, then succeeds."""
+ def get_image(self, image_id):
+ info['num_calls'] += 1
+ if info['num_calls'] == 1:
+ raise glance_exception.ClientConnectionError()
+ return {}, []
+
+ return MyGlanceStubClient()
+
+
+class TestGlanceClientWrapper(test.TestCase):
+
+ def setUp(self):
+ super(TestGlanceClientWrapper, self).setUp()
+ self.flags(glance_api_servers=['host1:9292', 'host2:9293',
+ 'host3:9294'])
+
+ # Make the test run fast
+ def _fake_sleep(secs):
+ pass
+ self.stubs.Set(time, 'sleep', _fake_sleep)
+
+ def test_static_client_without_retries(self):
+ self.flags(glance_num_retries=0)
+
+ ctxt = context.RequestContext('fake', 'fake')
+ fake_host = 'host4'
+ fake_port = 9295
+
+ info = {'num_calls': 0}
+
+ def _fake_create_glance_client(context, host, port):
+ self.assertEqual(host, fake_host)
+ self.assertEqual(port, fake_port)
+ return _create_failing_glance_client(info)
+
+ self.stubs.Set(glance, '_create_glance_client',
+ _fake_create_glance_client)
+
+ client = glance.GlanceClientWrapper(context=ctxt,
+ host=fake_host, port=fake_port)
+ self.assertRaises(exception.GlanceConnectionFailed,
+ client.call, ctxt, 'get_image', 'meow')
+ self.assertEqual(info['num_calls'], 1)
+
+ def test_default_client_without_retries(self):
+ self.flags(glance_num_retries=0)
+
+ ctxt = context.RequestContext('fake', 'fake')
+
+ info = {'num_calls': 0,
+ 'host': 'host1',
+ 'port': 9292}
+
+ # Leave the list in a known-order
+ def _fake_shuffle(servers):
+ pass
+
+ def _fake_create_glance_client(context, host, port):
+ self.assertEqual(host, info['host'])
+ self.assertEqual(port, info['port'])
+ return _create_failing_glance_client(info)
+
+ self.stubs.Set(random, 'shuffle', _fake_shuffle)
+ self.stubs.Set(glance, '_create_glance_client',
+ _fake_create_glance_client)
+
+ client = glance.GlanceClientWrapper()
+ client2 = glance.GlanceClientWrapper()
+ self.assertRaises(exception.GlanceConnectionFailed,
+ client.call, ctxt, 'get_image', 'meow')
+ self.assertEqual(info['num_calls'], 1)
+
+ info = {'num_calls': 0,
+ 'host': 'host2',
+ 'port': 9293}
+
+ def _fake_shuffle2(servers):
+ # fake shuffle in a known manner
+ servers.append(servers.pop(0))
+
+ self.stubs.Set(random, 'shuffle', _fake_shuffle2)
+
+ self.assertRaises(exception.GlanceConnectionFailed,
+ client2.call, ctxt, 'get_image', 'meow')
+ self.assertEqual(info['num_calls'], 1)
+
+ def test_static_client_with_retries(self):
+ self.flags(glance_num_retries=1)
+
+ ctxt = context.RequestContext('fake', 'fake')
+ fake_host = 'host4'
+ fake_port = 9295
+
+ info = {'num_calls': 0}
+
+ def _fake_create_glance_client(context, host, port):
+ self.assertEqual(host, fake_host)
+ self.assertEqual(port, fake_port)
+ return _create_failing_glance_client(info)
+
+ self.stubs.Set(glance, '_create_glance_client',
+ _fake_create_glance_client)
+
+ client = glance.GlanceClientWrapper(context=ctxt,
+ host=fake_host, port=fake_port)
+ client.call(ctxt, 'get_image', 'meow')
+ self.assertEqual(info['num_calls'], 2)
+
+ def test_default_client_with_retries(self):
+ self.flags(glance_num_retries=1)
+
+ ctxt = context.RequestContext('fake', 'fake')
+
+ info = {'num_calls': 0,
+ 'host0': 'host1',
+ 'port0': 9292,
+ 'host1': 'host2',
+ 'port1': 9293}
+
+ # Leave the list in a known-order
+ def _fake_shuffle(servers):
+ pass
+
+ def _fake_create_glance_client(context, host, port):
+ attempt = info['num_calls']
+ self.assertEqual(host, info['host%s' % attempt])
+ self.assertEqual(port, info['port%s' % attempt])
+ return _create_failing_glance_client(info)
+
+ self.stubs.Set(random, 'shuffle', _fake_shuffle)
+ self.stubs.Set(glance, '_create_glance_client',
+ _fake_create_glance_client)
+
+ client = glance.GlanceClientWrapper()
+ client2 = glance.GlanceClientWrapper()
+ client.call(ctxt, 'get_image', 'meow')
+ self.assertEqual(info['num_calls'], 2)
+
+ def _fake_shuffle2(servers):
+ # fake shuffle in a known manner
+ servers.append(servers.pop(0))
+
+ self.stubs.Set(random, 'shuffle', _fake_shuffle2)
+
+ info = {'num_calls': 0,
+ 'host0': 'host2',
+ 'port0': 9293,
+ 'host1': 'host3',
+ 'port1': 9294}
+
+ client2.call(ctxt, 'get_image', 'meow')
+ self.assertEqual(info['num_calls'], 2)
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index df35d54ba..069a98c14 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -20,7 +20,6 @@ import ast
import contextlib
import cPickle as pickle
import functools
-import mox
import os
import re
@@ -31,7 +30,6 @@ from nova import context
from nova import db
from nova import exception
from nova import flags
-from nova.image import glance
from nova.openstack.common import importutils
from nova.openstack.common import jsonutils
from nova.openstack.common import log as logging
@@ -2053,8 +2051,6 @@ class VmUtilsTestCase(test.TestCase):
def test_upload_image(self):
"""Ensure image properties include instance system metadata
as well as few local settings."""
- def fake_pick_glance_api_server():
- return ("host", 80)
def fake_instance_system_metadata_get(context, uuid):
return dict(image_a=1, image_b=2, image_c='c', d='d')
@@ -2076,8 +2072,6 @@ class VmUtilsTestCase(test.TestCase):
def fake_dumps(thing):
return thing
- self.stubs.Set(glance, "pick_glance_api_server",
- fake_pick_glance_api_server)
self.stubs.Set(db, "instance_system_metadata_get",
fake_instance_system_metadata_get)
self.stubs.Set(vm_utils, "get_sr_path", fake_get_sr_path)
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 9d340861d..bfbffd8cd 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -560,7 +560,8 @@ def upload_image(context, session, instance, vdi_uuids, image_id):
LOG.debug(_("Asking xapi to upload %(vdi_uuids)s as"
" ID %(image_id)s"), locals(), instance=instance)
- glance_host, glance_port = glance.pick_glance_api_server()
+ glance_api_servers = glance.get_api_servers()
+ glance_host, glance_port = glance_api_servers.next()
# TODO(sirp): this inherit-image-property code should probably go in
# nova/compute/manager so it can be shared across hypervisors
@@ -916,8 +917,10 @@ def _fetch_vhd_image(context, session, instance, image_id):
'sr_path': get_sr_path(session),
'auth_token': getattr(context, 'auth_token', None)}
+ glance_api_servers = glance.get_api_servers()
+
def pick_glance(params):
- glance_host, glance_port = glance.pick_glance_api_server()
+ glance_host, glance_port = glance_api_servers.next()
params['glance_host'] = glance_host
params['glance_port'] = glance_port