diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index 35c4723c63..ffd3a0be44 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -2244,6 +2244,40 @@ #xenapi_login_timeout=10 +# +# Options defined in nova.virt.xenapi.image.bittorrent +# + +# Base URL for torrent files. (string value) +#xenapi_torrent_base_url= + +# Probability that peer will become a seeder. (1.0 = 100%) +# (floating point value) +#xenapi_torrent_seed_chance=1.0 + +# Number of seconds after downloading an image via BitTorrent +# that it should be seeded for other peers. (integer value) +#xenapi_torrent_seed_duration=3600 + +# Cached torrent files not accessed within this number of +# seconds can be reaped (integer value) +#xenapi_torrent_max_last_accessed=86400 + +# Beginning of port range to listen on (integer value) +#xenapi_torrent_listen_port_start=6881 + +# End of port range to listen on (integer value) +#xenapi_torrent_listen_port_end=6891 + +# Number of seconds a download can remain at the same progress +# percentage w/o being considered a stall (integer value) +#xenapi_torrent_download_stall_cutoff=600 + +# Maximum number of seeder processes to run concurrently +# within a given dom0. (-1 = no limit) (integer value) +#xenapi_torrent_max_seeder_processes_per_host=1 + + # # Options defined in nova.virt.xenapi.pool # @@ -2305,35 +2339,6 @@ # (all|some|none). (string value) #xenapi_torrent_images=none -# Base URL for torrent files. (string value) -#xenapi_torrent_base_url= - -# Probability that peer will become a seeder. (1.0 = 100%) -# (floating point value) -#xenapi_torrent_seed_chance=1.0 - -# Number of seconds after downloading an image via BitTorrent -# that it should be seeded for other peers. (integer value) -#xenapi_torrent_seed_duration=3600 - -# Cached torrent files not accessed within this number of -# seconds can be reaped (integer value) -#xenapi_torrent_max_last_accessed=86400 - -# Beginning of port range to listen on (integer value) -#xenapi_torrent_listen_port_start=6881 - -# End of port range to listen on (integer value) -#xenapi_torrent_listen_port_end=6891 - -# Number of seconds a download can remain at the same progress -# percentage w/o being considered a stall (integer value) -#xenapi_torrent_download_stall_cutoff=600 - -# Maximum number of seeder processes to run concurrently -# within a given dom0. (-1 = no limit) (integer value) -#xenapi_torrent_max_seeder_processes_per_host=1 - # # Options defined in nova.virt.xenapi.vmops @@ -2347,7 +2352,7 @@ # value) #xenapi_vif_driver=nova.virt.xenapi.vif.XenAPIBridgeDriver -# Object Store Driver used to handle image uploads. (string +# Dom0 plugin driver used to handle image uploads. (string # value) #xenapi_image_upload_handler=nova.virt.xenapi.image.glance.GlanceStore diff --git a/nova/tests/virt/xenapi/image/test_bittorrent.py b/nova/tests/virt/xenapi/image/test_bittorrent.py new file mode 100644 index 0000000000..a617ff5b43 --- /dev/null +++ b/nova/tests/virt/xenapi/image/test_bittorrent.py @@ -0,0 +1,147 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +import pkg_resources + +import mox + +from nova import context +from nova.openstack.common.gettextutils import _ +from nova import test +from nova.tests.virt.xenapi import stubs +from nova.virt.xenapi import driver as xenapi_conn +from nova.virt.xenapi import fake +from nova.virt.xenapi.image import bittorrent +from nova.virt.xenapi import vm_utils + + +class TestBittorrentStore(stubs.XenAPITestBase): + def setUp(self): + super(TestBittorrentStore, self).setUp() + self.store = bittorrent.BittorrentStore() + self.mox = mox.Mox() + + self.flags(xenapi_torrent_base_url='http://foo', + xenapi_connection_url='test_url', + xenapi_connection_password='test_pass') + + self.context = context.RequestContext( + 'user', 'project', auth_token='foobar') + + fake.reset() + stubs.stubout_session(self.stubs, fake.SessionBase) + driver = xenapi_conn.XenAPIDriver(False) + self.session = driver._session + + self.stubs.Set( + vm_utils, 'get_sr_path', lambda *a, **kw: '/fake/sr/path') + + self.instance = {'uuid': 'blah', + 'system_metadata': {'image_xenapi_use_agent': 'true'}, + 'auto_disk_config': True, + 'os_type': 'default', + 'xenapi_use_agent': 'true'} + + def test_download_image(self): + params = {'image_id': 'fake_image_uuid', + 'sr_path': '/fake/sr/path', + 'torrent_download_stall_cutoff': 600, + 'torrent_listen_port_end': 6891, + 'torrent_listen_port_start': 6881, + 'torrent_max_last_accessed': 86400, + 'torrent_max_seeder_processes_per_host': 1, + 'torrent_seed_chance': 1.0, + 'torrent_seed_duration': 3600, + 'torrent_url': 'http://foo/fake_image_uuid.torrent', + 'uuid_stack': ['uuid1']} + + self.stubs.Set(vm_utils, '_make_uuid_stack', + lambda *a, **kw: ['uuid1']) + + self.mox.StubOutWithMock(self.session, 'call_plugin_serialized') + self.session.call_plugin_serialized( + 'bittorrent', 'download_vhd', **params) + self.mox.ReplayAll() + + vdis = self.store.download_image( + self.context, self.session, self.instance, 'fake_image_uuid') + + self.mox.VerifyAll() + + def test_upload_image(self): + self.assertRaises(NotImplementedError, self.store.upload_image, + self.context, self.session, self.instance, ['fake_vdi_uuid'], + 'fake_image_uuid') + + +def bad_fetcher(instance, image_id): + raise test.TestingException("just plain bad.") + + +def another_fetcher(instance, image_id): + return "http://www.foobar.com/%s" % image_id + + +class MockEntryPoint(object): + name = "torrent_url" + + def load(self): + return another_fetcher + + +class LookupTorrentURLTestCase(test.TestCase): + def setUp(self): + super(LookupTorrentURLTestCase, self).setUp() + self.store = bittorrent.BittorrentStore() + self.instance = {'uuid': 'fakeuuid'} + self.image_id = 'fakeimageid' + + def test_default_fetch_url_no_base_url_set(self): + self.flags(xenapi_torrent_base_url=None) + exc = self.assertRaises( + RuntimeError, self.store._lookup_torrent_url_fn) + self.assertEqual(_('Cannot create default bittorrent URL without' + ' xenapi_torrent_base_url set'), + str(exc)) + + def test_default_fetch_url_base_url_is_set(self): + self.flags(xenapi_torrent_base_url='http://foo') + lookup_fn = self.store._lookup_torrent_url_fn() + self.assertEqual('http://foo/fakeimageid.torrent', + lookup_fn(self.instance, self.image_id)) + + def test_with_extension(self): + def mock_iter_single(namespace): + return [MockEntryPoint()] + + self.stubs.Set(pkg_resources, 'iter_entry_points', mock_iter_single) + + lookup_fn = self.store._lookup_torrent_url_fn() + self.assertEqual("http://www.foobar.com/%s" % self.image_id, + lookup_fn(self.instance, self.image_id)) + + def test_multiple_extensions_found(self): + def mock_iter_multiple(namespace): + return [MockEntryPoint(), MockEntryPoint()] + + self.stubs.Set(pkg_resources, 'iter_entry_points', mock_iter_multiple) + + exc = self.assertRaises( + RuntimeError, self.store._lookup_torrent_url_fn) + self.assertEqual(_('Multiple torrent URL fetcher extension found.' + ' Failing.'), + str(exc)) diff --git a/nova/tests/virt/xenapi/test_vm_utils.py b/nova/tests/virt/xenapi/test_vm_utils.py index 6189e27873..dc25275263 100644 --- a/nova/tests/virt/xenapi/test_vm_utils.py +++ b/nova/tests/virt/xenapi/test_vm_utils.py @@ -16,8 +16,6 @@ # under the License. import contextlib -import pkg_resources -import urlparse import fixtures import mox @@ -210,10 +208,13 @@ class XenAPIGetUUID(test.TestCase): self.mox.VerifyAll() -class FakeSession(): +class FakeSession(object): def call_xenapi(self, *args): pass + def call_plugin_serialized(self, plugin, fn, *args, **kwargs): + pass + def call_plugin_serialized_with_retry(self, plugin, fn, num_retries, callback, *args, **kwargs): pass @@ -224,101 +225,123 @@ class FetchVhdImageTestCase(test.TestCase): super(FetchVhdImageTestCase, self).setUp() self.context = context.get_admin_context() self.context.auth_token = 'auth_token' - self.session = FakeSession() self.instance = {"uuid": "uuid"} - self.image_id = "image_id" - self.uuid_stack = ["uuid_stack"] - self.sr_path = "sr_path" - self.params = {'image_id': self.image_id, - 'uuid_stack': self.uuid_stack, - 'sr_path': self.sr_path} - self.vdis = {'root': {'uuid': 'vdi'}} self.mox.StubOutWithMock(vm_utils, '_make_uuid_stack') - self.mox.StubOutWithMock(vm_utils, 'get_sr_path') - self.mox.StubOutWithMock(vm_utils, '_image_uses_bittorrent') - self.mox.StubOutWithMock(vm_utils, '_add_bittorrent_params') - self.mox.StubOutWithMock(vm_utils, 'safe_find_sr') - self.mox.StubOutWithMock(vm_utils, '_scan_sr') - self.mox.StubOutWithMock(vm_utils, '_check_vdi_size') - self.mox.StubOutWithMock(vm_utils, 'destroy_vdi') - self.mox.StubOutWithMock( - self.session, 'call_plugin_serialized_with_retry') - self.mox.StubOutWithMock(vm_utils, '_add_torrent_url') + vm_utils._make_uuid_stack().AndReturn(["uuid_stack"]) - vm_utils._make_uuid_stack().AndReturn(self.uuid_stack) - vm_utils.get_sr_path(self.session).AndReturn(self.sr_path) + self.mox.StubOutWithMock(vm_utils, 'get_sr_path') + vm_utils.get_sr_path(self.session).AndReturn('sr_path') def test_fetch_vhd_image_works_with_glance(self): + self.mox.StubOutWithMock(vm_utils, '_image_uses_bittorrent') vm_utils._image_uses_bittorrent( self.context, self.instance).AndReturn(False) - self.params['auth_token'] = 'auth_token' + self.mox.StubOutWithMock( + self.session, 'call_plugin_serialized_with_retry') self.session.call_plugin_serialized_with_retry( 'glance', 'download_vhd', 0, mox.IgnoreArg(), - **self.params).AndReturn(self.vdis) + auth_token='auth_token', + image_id='image_id', + uuid_stack=["uuid_stack"], + sr_path='sr_path').AndReturn({'root': {'uuid': 'vdi'}}) + self.mox.StubOutWithMock(vm_utils, 'safe_find_sr') vm_utils.safe_find_sr(self.session).AndReturn("sr") + + self.mox.StubOutWithMock(vm_utils, '_scan_sr') vm_utils._scan_sr(self.session, "sr") + + self.mox.StubOutWithMock(vm_utils, '_check_vdi_size') vm_utils._check_vdi_size( self.context, self.session, self.instance, "vdi") self.mox.ReplayAll() self.assertEqual("vdi", vm_utils._fetch_vhd_image(self.context, - self.session, self.instance, self.image_id)['root']['uuid']) + self.session, self.instance, 'image_id')['root']['uuid']) self.mox.VerifyAll() - def _setup_bittorrent(self): + def test_fetch_vhd_image_works_with_bittorrent(self): + cfg.CONF.import_opt('xenapi_torrent_base_url', + 'nova.virt.xenapi.image.bittorrent') + self.flags(xenapi_torrent_base_url='http://foo') + + self.mox.StubOutWithMock(vm_utils, '_image_uses_bittorrent') vm_utils._image_uses_bittorrent( self.context, self.instance).AndReturn(True) - def _add_torrent_url(instance, image_id, params): - params['torrent_url'] = "%s.torrent" % image_id - - vm_utils._add_torrent_url( - self.instance, self.image_id, mox.IgnoreArg()).AndReturn(True) - - vm_utils._add_bittorrent_params(self.image_id, self.params) - - self.session.call_plugin_serialized_with_retry( - 'bittorrent', 'download_vhd', 0, None, - **self.params).AndReturn(self.vdis) - - def test_fetch_vhd_image_works_with_bittorrent(self): - self._setup_bittorrent() + self.mox.StubOutWithMock( + self.session, 'call_plugin_serialized') + self.session.call_plugin_serialized('bittorrent', 'download_vhd', + image_id='image_id', + uuid_stack=["uuid_stack"], + sr_path='sr_path', + torrent_download_stall_cutoff=600, + torrent_listen_port_start=6881, + torrent_listen_port_end=6891, + torrent_max_last_accessed=86400, + torrent_max_seeder_processes_per_host=1, + torrent_seed_chance=1.0, + torrent_seed_duration=3600, + torrent_url='http://foo/image_id.torrent' + ).AndReturn({'root': {'uuid': 'vdi'}}) + self.mox.StubOutWithMock(vm_utils, 'safe_find_sr') vm_utils.safe_find_sr(self.session).AndReturn("sr") + + self.mox.StubOutWithMock(vm_utils, '_scan_sr') vm_utils._scan_sr(self.session, "sr") + + self.mox.StubOutWithMock(vm_utils, '_check_vdi_size') vm_utils._check_vdi_size(self.context, self.session, self.instance, "vdi") self.mox.ReplayAll() self.assertEqual("vdi", vm_utils._fetch_vhd_image(self.context, - self.session, self.instance, self.image_id)['root']['uuid']) + self.session, self.instance, 'image_id')['root']['uuid']) self.mox.VerifyAll() def test_fetch_vhd_image_cleans_up_vdi_on_fail(self): - self.mox.StubOutWithMock(self.session, 'call_xenapi') - self._setup_bittorrent() + self.mox.StubOutWithMock(vm_utils, '_image_uses_bittorrent') + vm_utils._image_uses_bittorrent( + self.context, self.instance).AndReturn(False) + self.mox.StubOutWithMock( + self.session, 'call_plugin_serialized_with_retry') + self.session.call_plugin_serialized_with_retry( + 'glance', 'download_vhd', 0, mox.IgnoreArg(), + auth_token='auth_token', + image_id='image_id', + uuid_stack=["uuid_stack"], + sr_path='sr_path').AndReturn({'root': {'uuid': 'vdi'}}) + + self.mox.StubOutWithMock(vm_utils, 'safe_find_sr') vm_utils.safe_find_sr(self.session).AndReturn("sr") + + self.mox.StubOutWithMock(vm_utils, '_scan_sr') vm_utils._scan_sr(self.session, "sr") + + self.mox.StubOutWithMock(vm_utils, '_check_vdi_size') vm_utils._check_vdi_size(self.context, self.session, self.instance, "vdi").AndRaise(exception.InstanceTypeDiskTooSmall) + self.mox.StubOutWithMock(self.session, 'call_xenapi') self.session.call_xenapi("VDI.get_by_uuid", "vdi").AndReturn("ref") + + self.mox.StubOutWithMock(vm_utils, 'destroy_vdi') vm_utils.destroy_vdi(self.session, "ref") self.mox.ReplayAll() self.assertRaises(exception.InstanceTypeDiskTooSmall, vm_utils._fetch_vhd_image, self.context, self.session, - self.instance, self.image_id) + self.instance, 'image_id') self.mox.VerifyAll() @@ -846,68 +869,6 @@ class VDIOtherConfigTestCase(stubs.XenAPITestBase): self.assertEqual(expected, other_config) -def bad_fetcher(instance, image_id): - raise test.TestingException("just plain bad.") - - -def another_fetcher(instance, image_id): - return "http://www.foobar.com/%s" % image_id - - -class MockEntryPoint(object): - name = "torrent_url" - - def load(self): - return another_fetcher - - -class BitTorrentMiscTestCase(test.TestCase): - - def tearDown(self): - vm_utils._TORRENT_URL_FN = None - super(BitTorrentMiscTestCase, self).tearDown() - - def test_default_fetch_url(self): - def mock_iter_none(namespace): - return [] - self.stubs.Set(pkg_resources, 'iter_entry_points', mock_iter_none) - - image_id = "1-2-3-4-5" - params = {} - self.assertTrue(vm_utils._add_torrent_url({}, image_id, params)) - expected = urlparse.urljoin(CONF.xenapi_torrent_base_url, - "%s.torrent" % image_id) - self.assertEqual(expected, params['torrent_url']) - self.assertEqual(vm_utils.get_torrent_url, - vm_utils._TORRENT_URL_FN) - - def test_with_extension(self): - def mock_iter_single(namespace): - return [MockEntryPoint()] - self.stubs.Set(pkg_resources, 'iter_entry_points', mock_iter_single) - - image_id = "1-2-3-4-5" - params = {} - self.assertTrue(vm_utils._add_torrent_url({}, image_id, params)) - expected = "http://www.foobar.com/%s" % image_id - self.assertEqual(expected, params['torrent_url']) - self.assertEqual(another_fetcher, vm_utils._TORRENT_URL_FN) - - def test_more_than_one_extension(self): - def mock_iter_multiple(namespace): - return [MockEntryPoint(), MockEntryPoint()] - self.stubs.Set(pkg_resources, 'iter_entry_points', mock_iter_multiple) - image_id = "1-2-3-4-5" - params = {} - self.assertRaises(RuntimeError, vm_utils._add_torrent_url, {}, - image_id, params) - - def test_fetch_url_failure(self): - # fetcher function fails: - vm_utils._TORRENT_URL_FN = bad_fetcher - self.assertFalse(vm_utils._add_torrent_url({}, '1-2-3-4-5', {})) - - class GenerateDiskTestCase(stubs.XenAPITestBase): def setUp(self): super(GenerateDiskTestCase, self).setUp() diff --git a/nova/virt/xenapi/image/bittorrent.py b/nova/virt/xenapi/image/bittorrent.py new file mode 100644 index 0000000000..3d33175ad7 --- /dev/null +++ b/nova/virt/xenapi/image/bittorrent.py @@ -0,0 +1,123 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +import pkg_resources +import urlparse + +from oslo.config import cfg + +from nova.openstack.common.gettextutils import _ +import nova.openstack.common.log as logging +from nova.virt.xenapi import vm_utils + +LOG = logging.getLogger(__name__) + +xenapi_torrent_opts = [ + cfg.StrOpt('xenapi_torrent_base_url', + default=None, + help='Base URL for torrent files.'), + cfg.FloatOpt('xenapi_torrent_seed_chance', + default=1.0, + help='Probability that peer will become a seeder.' + ' (1.0 = 100%)'), + cfg.IntOpt('xenapi_torrent_seed_duration', + default=3600, + help='Number of seconds after downloading an image via' + ' BitTorrent that it should be seeded for other peers.'), + cfg.IntOpt('xenapi_torrent_max_last_accessed', + default=86400, + help='Cached torrent files not accessed within this number of' + ' seconds can be reaped'), + cfg.IntOpt('xenapi_torrent_listen_port_start', + default=6881, + help='Beginning of port range to listen on'), + cfg.IntOpt('xenapi_torrent_listen_port_end', + default=6891, + help='End of port range to listen on'), + cfg.IntOpt('xenapi_torrent_download_stall_cutoff', + default=600, + help='Number of seconds a download can remain at the same' + ' progress percentage w/o being considered a stall'), + cfg.IntOpt('xenapi_torrent_max_seeder_processes_per_host', + default=1, + help='Maximum number of seeder processes to run concurrently' + ' within a given dom0. (-1 = no limit)') + ] + +CONF = cfg.CONF +CONF.register_opts(xenapi_torrent_opts) + + +class BittorrentStore(object): + @staticmethod + def _lookup_torrent_url_fn(): + """Load a "fetcher" func to get the right torrent URL via + entrypoints. + """ + matches = [ep for ep in + pkg_resources.iter_entry_points('nova.virt.xenapi.vm_utils') + if ep.name == 'torrent_url'] + + if not matches: + LOG.debug(_("No torrent URL fetcher extension found, using" + " default.")) + + if not CONF.xenapi_torrent_base_url: + raise RuntimeError(_('Cannot create default bittorrent URL' + ' without xenapi_torrent_base_url set')) + + def _default_torrent_url_fn(instance, image_id): + return urlparse.urljoin(CONF.xenapi_torrent_base_url, + "%s.torrent" % image_id) + + fn = _default_torrent_url_fn + elif len(matches) > 1: + raise RuntimeError(_("Multiple torrent URL fetcher extension" + " found. Failing.")) + else: + ep = matches[0] + LOG.debug(_("Loading torrent URL fetcher from entry points" + " %(ep)s"), {'ep': ep}) + fn = ep.load() + + return fn + + def download_image(self, context, session, instance, image_id): + params = {} + params['image_id'] = image_id + params['uuid_stack'] = vm_utils._make_uuid_stack() + params['sr_path'] = vm_utils.get_sr_path(session) + params['torrent_seed_duration'] = CONF.xenapi_torrent_seed_duration + params['torrent_seed_chance'] = CONF.xenapi_torrent_seed_chance + params['torrent_max_last_accessed'] = \ + CONF.xenapi_torrent_max_last_accessed + params['torrent_listen_port_start'] = \ + CONF.xenapi_torrent_listen_port_start + params['torrent_listen_port_end'] = CONF.xenapi_torrent_listen_port_end + params['torrent_download_stall_cutoff'] = \ + CONF.xenapi_torrent_download_stall_cutoff + params['torrent_max_seeder_processes_per_host'] = \ + CONF.xenapi_torrent_max_seeder_processes_per_host + + lookup_fn = self._lookup_torrent_url_fn() + params['torrent_url'] = lookup_fn(instance, image_id) + + vdis = session.call_plugin_serialized( + 'bittorrent', 'download_vhd', **params) + + return vdis + + def upload_image(self, context, session, instance, vdi_uuids, image_id): + raise NotImplementedError diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 2374ca8ea1..11541e6e80 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -23,7 +23,6 @@ their attributes like VDIs, VIFs, as well as their lookup functions. import contextlib import os -import pkg_resources import re import time import urllib @@ -98,35 +97,6 @@ xenapi_vm_utils_opts = [ default='none', help='Whether or not to download images via Bit Torrent ' '(all|some|none).'), - cfg.StrOpt('xenapi_torrent_base_url', - default=None, - help='Base URL for torrent files.'), - cfg.FloatOpt('xenapi_torrent_seed_chance', - default=1.0, - help='Probability that peer will become a seeder.' - ' (1.0 = 100%)'), - cfg.IntOpt('xenapi_torrent_seed_duration', - default=3600, - help='Number of seconds after downloading an image via' - ' BitTorrent that it should be seeded for other peers.'), - cfg.IntOpt('xenapi_torrent_max_last_accessed', - default=86400, - help='Cached torrent files not accessed within this number of' - ' seconds can be reaped'), - cfg.IntOpt('xenapi_torrent_listen_port_start', - default=6881, - help='Beginning of port range to listen on'), - cfg.IntOpt('xenapi_torrent_listen_port_end', - default=6891, - help='End of port range to listen on'), - cfg.IntOpt('xenapi_torrent_download_stall_cutoff', - default=600, - help='Number of seconds a download can remain at the same' - ' progress percentage w/o being considered a stall'), - cfg.IntOpt('xenapi_torrent_max_seeder_processes_per_host', - default=1, - help='Maximum number of seeder processes to run concurrently' - ' within a given dom0. (-1 = no limit)') ] CONF = cfg.CONF @@ -1138,6 +1108,20 @@ def _image_uses_bittorrent(context, instance): return bittorrent +def _default_download_handler(): + # TODO(sirp): This should be configurable like upload_handler + return importutils.import_object( + 'nova.virt.xenapi.image.glance.GlanceStore') + + +def _choose_download_handler(context, instance): + if _image_uses_bittorrent(context, instance): + return importutils.import_object( + 'nova.virt.xenapi.image.bittorrent.BittorrentStore') + else: + return _default_download_handler() + + def _fetch_vhd_image(context, session, instance, image_id): """Tell glance to download an image and put the VHDs into the SR @@ -1146,25 +1130,23 @@ def _fetch_vhd_image(context, session, instance, image_id): LOG.debug(_("Asking xapi to fetch vhd image %s"), image_id, instance=instance) - params = {} - if (_image_uses_bittorrent(context, instance) and - _add_torrent_url(instance, image_id, params)): - params.update({'image_id': image_id, - 'uuid_stack': _make_uuid_stack(), - 'sr_path': get_sr_path(session)}) - _add_bittorrent_params(image_id, params) - try: - vdis = session.call_plugin_serialized_with_retry( - 'bittorrent', 'download_vhd', CONF.glance_num_retries, - None, **params) - except exception.PluginRetriesExceeded: - raise exception.CouldNotFetchImage(image_id=image_id) + handler = _choose_download_handler(context, instance) - else: - download_handler = importutils.import_object( - 'nova.virt.xenapi.image.glance.GlanceStore') + try: + vdis = handler.download_image(context, session, instance, image_id) + except Exception as e: + default_handler = _default_download_handler() - vdis = download_handler.download_image( + if handler == default_handler: + raise + + LOG.exception(_("Download handler '%(handler)s' raised an" + " exception, falling back to default handler" + " '%(default_handler)s'") % + {'handler': handler, + 'default_handler': default_handler}) + + vdis = default_handler.download_image( context, session, instance, image_id) sr_ref = safe_find_sr(session) @@ -1183,76 +1165,6 @@ def _fetch_vhd_image(context, session, instance, image_id): return vdis -_TORRENT_URL_FN = None # driver function to determine torrent URL to use - - -def _lookup_torrent_url_fn(): - """Load a "fetcher" func to get the right torrent URL via entrypoints.""" - namespace = "nova.virt.xenapi.vm_utils" - name = "torrent_url" - - eps = pkg_resources.iter_entry_points(namespace) - eps = [ep for ep in eps if ep.name == name] - - x = len(eps) - - if x == 0: - LOG.debug(_("No torrent URL fetcher extension found.")) - return None - elif x > 1: - raise RuntimeError(_("Multiple torrent URL fetcher extension found. " - "Failing.")) - - ep = eps[0] - LOG.debug(_("Loading torrent URL fetcher from entry points %(ep)s"), - {'ep': ep}) - fn = ep.load() - return fn - - -def _add_torrent_url(instance, image_id, params): - """Add the torrent URL associated with the given image. - - :param instance: instance ref - :param image_id: unique id of image - :param params: BT params dict - :returns: True if the URL could be obtained - """ - global _TORRENT_URL_FN - if not _TORRENT_URL_FN: - fn = _lookup_torrent_url_fn() - if fn is None: - LOG.debug(_("No torrent URL fetcher installed.")) - _TORRENT_URL_FN = get_torrent_url # default - else: - _TORRENT_URL_FN = fn - - try: - url = _TORRENT_URL_FN(instance, image_id) - params['torrent_url'] = url - return True - except Exception: - LOG.exception(_("Failed to get torrent URL for image %s") % image_id) - return False # fall back to using glance - - -def get_torrent_url(instance, image_id): - return urlparse.urljoin(CONF.xenapi_torrent_base_url, - "%s.torrent" % image_id) - - -def _add_bittorrent_params(image_id, params): - params['torrent_seed_duration'] = CONF.xenapi_torrent_seed_duration - params['torrent_seed_chance'] = CONF.xenapi_torrent_seed_chance - params['torrent_max_last_accessed'] = CONF.xenapi_torrent_max_last_accessed - params['torrent_listen_port_start'] = CONF.xenapi_torrent_listen_port_start - params['torrent_listen_port_end'] = CONF.xenapi_torrent_listen_port_end - params['torrent_download_stall_cutoff'] = \ - CONF.xenapi_torrent_download_stall_cutoff - params['torrent_max_seeder_processes_per_host'] = \ - CONF.xenapi_torrent_max_seeder_processes_per_host - - def _get_vdi_chain_size(session, vdi_uuid): """Compute the total size of a VDI chain, starting with the specified VDI UUID. diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index fb3b0572f3..32d6b669a6 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -67,7 +67,7 @@ xenapi_vmops_opts = [ help='The XenAPI VIF driver using XenServer Network APIs.'), cfg.StrOpt('xenapi_image_upload_handler', default='nova.virt.xenapi.image.glance.GlanceStore', - help='Object Store Driver used to handle image uploads.'), + help='Dom0 plugin driver used to handle image uploads.'), ] CONF = cfg.CONF