Adds new volume API extensions

Adds following extensions:
1. Create volume from image
2. Copy volume to image

Added unit tests.

Implements: blueprint create-volume-from-image

Conflicts:

	cinder/api/openstack/volume/contrib/volume_actions.py
	cinder/tests/api/openstack/fakes.py
	cinder/tests/api/openstack/volume/contrib/test_volume_actions.py
	cinder/tests/policy.json
	nova/api/openstack/volume/volumes.py
	nova/flags.py
	nova/tests/api/openstack/volume/test_volumes.py
	nova/tests/test_volume.py
	nova/utils.py
	nova/volume/api.py
	nova/volume/manager.py

This is based on a cherry-pick of cinder commit
2f5360753308eb8b10581fc3c026c1b66f42ebdc with bug fixes
8c30edff982042d2533a810709308b586267c0e9 and
ffe5036fa0e63ccde2d19aa0f425ec43de338dd7 squashed in.

Change-Id: I9c73bd3fa2fa2e0648c01ff3f4fc66f757d7bc3f
This commit is contained in:
Unmesh Gurjar 2012-08-11 10:31:51 -07:00 committed by Josh Durgin
parent 40933a9aeb
commit 3487ece9cb
10 changed files with 266 additions and 16 deletions

View File

@ -93,10 +93,10 @@ class ChanceScheduler(driver.Scheduler):
self.compute_rpcapi.prep_resize(context, image, instance,
instance_type, host, reservations)
def schedule_create_volume(self, context, volume_id, snapshot_id,
def schedule_create_volume(self, context, volume_id, snapshot_id, image_id,
reservations):
"""Picks a host that is up at random."""
host = self._schedule(context, FLAGS.volume_topic, None, {})
driver.cast_to_host(context, FLAGS.volume_topic, host, 'create_volume',
volume_id=volume_id, snapshot_id=snapshot_id,
reservations=reservations)
image_id=image_id, reservations=reservations)

View File

@ -207,7 +207,7 @@ class Scheduler(object):
msg = _("Driver must implement schedule_run_instance")
raise NotImplementedError(msg)
def schedule_create_volume(self, context, volume_id, snapshot_id,
def schedule_create_volume(self, context, volume_id, snapshot_id, image_id,
reservations):
msg = _("Driver must implement schedule_create_volune")
raise NotImplementedError(msg)

View File

@ -42,7 +42,7 @@ class FilterScheduler(driver.Scheduler):
self.cost_function_cache = {}
self.options = scheduler_options.SchedulerOptions()
def schedule_create_volume(self, context, volume_id, snapshot_id,
def schedule_create_volume(self, context, volume_id, snapshot_id, image_id,
reservations):
# NOTE: We're only focused on compute instances right now,
# so this method will always raise NoValidHost().

View File

@ -69,10 +69,11 @@ class SchedulerManager(manager.Manager):
self.driver.update_service_capabilities(service_name, host,
capabilities)
def create_volume(self, context, volume_id, snapshot_id, reservations):
def create_volume(self, context, volume_id, snapshot_id, image_id,
reservations):
try:
self.driver.schedule_create_volume(
context, volume_id, snapshot_id, reservations)
context, volume_id, snapshot_id, image_id, reservations)
except Exception as ex:
with excutils.save_and_reraise_exception():
self._set_vm_state_and_notify('create_volume',

View File

@ -93,11 +93,12 @@ class SchedulerAPI(nova.openstack.common.rpc.proxy.RpcProxy):
disk_over_commit=disk_over_commit, instance=instance_p,
dest=dest))
def create_volume(self, ctxt, volume_id, snapshot_id, reservations):
def create_volume(self, ctxt, volume_id, snapshot_id, image_id,
reservations):
self.cast(ctxt,
self.make_msg('create_volume',
volume_id=volume_id, snapshot_id=snapshot_id,
reservations=reservations))
image_id=image_id, reservations=reservations))
def update_service_capabilities(self, ctxt, service_name, host,
capabilities):

View File

@ -56,7 +56,7 @@ class SimpleScheduler(chance.ChanceScheduler):
request_spec, admin_password, injected_files,
requested_networks, is_first_time, filter_properties)
def schedule_create_volume(self, context, volume_id, snapshot_id,
def schedule_create_volume(self, context, volume_id, snapshot_id, image_id,
reservations):
"""Picks a host that is up and has the fewest volumes."""
deprecated.warn(_('nova-volume functionality is deprecated in Folsom '
@ -76,7 +76,7 @@ class SimpleScheduler(chance.ChanceScheduler):
raise exception.WillNotSchedule(host=host)
driver.cast_to_volume_host(context, host, 'create_volume',
volume_id=volume_id, snapshot_id=snapshot_id,
reservations=reservations)
image_id=image_id, reservations=reservations)
return None
results = db.service_get_all_volume_sorted(elevated)
@ -91,7 +91,8 @@ class SimpleScheduler(chance.ChanceScheduler):
if utils.service_is_up(service) and not service['disabled']:
driver.cast_to_volume_host(context, service['host'],
'create_volume', volume_id=volume_id,
snapshot_id=snapshot_id, reservations=reservations)
snapshot_id=snapshot_id, image_id=image_id,
reservations=reservations)
return None
msg = _("Is the appropriate service running?")
raise exception.NoValidHost(reason=msg)

View File

@ -154,6 +154,7 @@
"volume_extension:volume_admin_actions:reset_status": [["rule:admin_api"]],
"volume_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]],
"volume_extension:volume_admin_actions:force_delete": [["rule:admin_api"]],
"volume_extension:volume_actions:upload_image": [],
"volume_extension:types_manage": [],
"volume_extension:types_extra_specs": [],

View File

@ -94,4 +94,5 @@ class SchedulerRpcAPITestCase(test.TestCase):
def test_create_volume(self):
self._test_scheduler_api('create_volume',
rpc_method='cast', volume_id="fake_volume",
snapshot_id="fake_snapshots", reservations=list('fake_res'))
snapshot_id="fake_snapshots", image_id="fake_image",
reservations=list('fake_res'))

View File

@ -21,8 +21,10 @@ Tests for Volume Code.
"""
import cStringIO
import datetime
import mox
import os
import shutil
import tempfile
@ -37,9 +39,13 @@ from nova.openstack.common import rpc
import nova.policy
from nova import quota
from nova import test
from nova.tests.image import fake as fake_image
import nova.volume.api
from nova.volume import iscsi
QUOTAS = quota.QUOTAS
FLAGS = flags.FLAGS
@ -60,11 +66,12 @@ class VolumeTestCase(test.TestCase):
self.instance_id = instance['id']
self.instance_uuid = instance['uuid']
test_notifier.NOTIFICATIONS = []
fake_image.stub_out_image_service(self.stubs)
def tearDown(self):
try:
shutil.rmtree(FLAGS.volumes_dir)
except OSError, e:
except OSError:
pass
db.instance_destroy(self.context, self.instance_uuid)
notifier_api._reset_drivers()
@ -74,11 +81,12 @@ class VolumeTestCase(test.TestCase):
return 1
@staticmethod
def _create_volume(size=0, snapshot_id=None, metadata=None):
def _create_volume(size=0, snapshot_id=None, image_id=None, metadata=None):
"""Create a volume object."""
vol = {}
vol['size'] = size
vol['snapshot_id'] = snapshot_id
vol['image_id'] = image_id
vol['user_id'] = 'fake'
vol['project_id'] = 'fake'
vol['availability_zone'] = FLAGS.storage_availability_zone
@ -137,7 +145,7 @@ class VolumeTestCase(test.TestCase):
def test_create_delete_volume_with_metadata(self):
"""Test volume can be created and deleted."""
test_meta = {'fake_key': 'fake_value'}
volume = self._create_volume('0', None, test_meta)
volume = self._create_volume('0', None, metadata=test_meta)
volume_id = volume['id']
self.volume.create_volume(self.context, volume_id)
result_meta = {
@ -564,6 +572,231 @@ class VolumeTestCase(test.TestCase):
volume = db.volume_get(self.context, volume['id'])
self.assertEqual(volume['status'], "in-use")
def _create_volume_from_image(self, expected_status,
fakeout_copy_image_to_volume=False):
"""Call copy image to volume, Test the status of volume after calling
copying image to volume."""
def fake_local_path(volume):
return dst_path
def fake_copy_image_to_volume(context, volume, image_id):
pass
dst_fd, dst_path = tempfile.mkstemp()
os.close(dst_fd)
self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
if fakeout_copy_image_to_volume:
self.stubs.Set(self.volume, '_copy_image_to_volume',
fake_copy_image_to_volume)
image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
volume_id = 1
# creating volume testdata
db.volume_create(self.context, {'id': volume_id,
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'display_description': 'Test Desc',
'size': 20,
'status': 'creating',
'instance_uuid': None,
'host': 'dummy'})
try:
self.volume.create_volume(self.context,
volume_id,
image_id=image_id)
volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], expected_status)
finally:
# cleanup
db.volume_destroy(self.context, volume_id)
os.unlink(dst_path)
def test_create_volume_from_image_status_downloading(self):
"""Verify that before copying image to volume, it is in downloading
state."""
self._create_volume_from_image('downloading', True)
def test_create_volume_from_image_status_available(self):
"""Verify that before copying image to volume, it is in available
state."""
self._create_volume_from_image('available')
def test_create_volume_from_image_exception(self):
"""Verify that create volume from image, the volume status is
'downloading'."""
dst_fd, dst_path = tempfile.mkstemp()
os.close(dst_fd)
self.stubs.Set(self.volume.driver, 'local_path', lambda x: dst_path)
image_id = 'aaaaaaaa-0000-0000-0000-000000000000'
# creating volume testdata
volume_id = 1
db.volume_create(self.context, {'id': volume_id,
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'display_description': 'Test Desc',
'size': 20,
'status': 'creating',
'host': 'dummy'})
self.assertRaises(exception.ImageNotFound,
self.volume.create_volume,
self.context,
volume_id,
None,
image_id)
volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], "error")
# cleanup
db.volume_destroy(self.context, volume_id)
os.unlink(dst_path)
def test_copy_volume_to_image_status_available(self):
dst_fd, dst_path = tempfile.mkstemp()
os.close(dst_fd)
def fake_local_path(volume):
return dst_path
self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
# creating volume testdata
volume_id = 1
db.volume_create(self.context, {'id': volume_id,
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'display_description': 'Test Desc',
'size': 20,
'status': 'uploading',
'instance_uuid': None,
'host': 'dummy'})
try:
# start test
self.volume.copy_volume_to_image(self.context,
volume_id,
image_id)
volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], 'available')
finally:
# cleanup
db.volume_destroy(self.context, volume_id)
os.unlink(dst_path)
def test_copy_volume_to_image_status_use(self):
dst_fd, dst_path = tempfile.mkstemp()
os.close(dst_fd)
def fake_local_path(volume):
return dst_path
self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
#image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
image_id = 'a440c04b-79fa-479c-bed1-0b816eaec379'
# creating volume testdata
volume_id = 1
db.volume_create(self.context,
{'id': volume_id,
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'display_description': 'Test Desc',
'size': 20,
'status': 'uploading',
'instance_uuid':
'b21f957d-a72f-4b93-b5a5-45b1161abb02',
'host': 'dummy'})
try:
# start test
self.volume.copy_volume_to_image(self.context,
volume_id,
image_id)
volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], 'in-use')
finally:
# cleanup
db.volume_destroy(self.context, volume_id)
os.unlink(dst_path)
def test_copy_volume_to_image_exception(self):
dst_fd, dst_path = tempfile.mkstemp()
os.close(dst_fd)
def fake_local_path(volume):
return dst_path
self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
image_id = 'aaaaaaaa-0000-0000-0000-000000000000'
# creating volume testdata
volume_id = 1
db.volume_create(self.context, {'id': volume_id,
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'display_description': 'Test Desc',
'size': 20,
'status': 'in-use',
'host': 'dummy'})
try:
# start test
self.assertRaises(exception.ImageNotFound,
self.volume.copy_volume_to_image,
self.context,
volume_id,
image_id)
volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], 'available')
finally:
# cleanup
db.volume_destroy(self.context, volume_id)
os.unlink(dst_path)
def test_create_volume_from_exact_sized_image(self):
"""Verify that an image which is exactly the same size as the
volume, will work correctly."""
class _FakeImageService:
def __init__(self, db_driver=None, image_service=None):
pass
def show(self, context, image_id):
return {'size': 2 * 1024 * 1024 * 1024}
image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
try:
volume_id = None
volume_api = nova.volume.api.API(
image_service=_FakeImageService())
volume = volume_api.create(self.context, 2, 'name', 'description',
image_id=1)
volume_id = volume['id']
self.assertEqual(volume['status'], 'creating')
finally:
# cleanup
db.volume_destroy(self.context, volume_id)
def test_create_volume_from_oversized_image(self):
"""Verify that an image which is too big will fail correctly."""
class _FakeImageService:
def __init__(self, db_driver=None, image_service=None):
pass
def show(self, context, image_id):
return {'size': 2 * 1024 * 1024 * 1024 + 1}
image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
volume_api = nova.volume.api.API(image_service=_FakeImageService())
self.assertRaises(exception.InvalidInput,
volume_api.create,
self.context, 2,
'name', 'description', image_id=1)
class DriverTestCase(test.TestCase):
"""Base Test class for Drivers."""
@ -591,7 +824,7 @@ class DriverTestCase(test.TestCase):
def tearDown(self):
try:
shutil.rmtree(FLAGS.volumes_dir)
except OSError, e:
except OSError:
pass
super(DriverTestCase, self).tearDown()

View File

@ -1135,6 +1135,18 @@ def read_cached_file(filename, cache_info, reload_func=None):
return cache_info['data']
def file_open(*args, **kwargs):
"""Open file
see built-in file() documentation for more details
Note: The reason this is kept in a separate module is to easily
be able to provide a stub module that doesn't alter system
state at all (for unit tests)
"""
return file(*args, **kwargs)
def hash_file(file_like_object):
"""Generate a hash for the contents of a file."""
checksum = hashlib.sha1()