Merge "xenapi: Adding BitTorrent download handler"
This commit is contained in:
@@ -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=<None>
|
||||
|
||||
# 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=<None>
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
147
nova/tests/virt/xenapi/image/test_bittorrent.py
Normal file
147
nova/tests/virt/xenapi/image/test_bittorrent.py
Normal file
@@ -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))
|
||||
@@ -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()
|
||||
|
||||
123
nova/virt/xenapi/image/bittorrent.py
Normal file
123
nova/virt/xenapi/image/bittorrent.py
Normal file
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user