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 f615e9c22c
commit de09c1866b
24 changed files with 916 additions and 48 deletions

View File

@ -31,3 +31,6 @@ iscsiadm_usr: CommandFilter, /usr/bin/iscsiadm, root
# nova/volume/driver.py
dmsetup: CommandFilter, /sbin/dmsetup, root
dmsetup_usr: CommandFilter, /usr/sbin/dmsetup, root
#nova/volume/.py: utils.temporary_chown(path, 0), ...
chown: CommandFilter, /bin/chown, root

View File

@ -761,14 +761,16 @@ class CloudController(object):
kwargs.get('size'),
context=context)
create_kwargs = dict(snapshot=snapshot,
volume_type=kwargs.get('volume_type'),
metadata=kwargs.get('metadata'),
availability_zone=kwargs.get('availability_zone'))
volume = self.volume_api.create(context,
kwargs.get('size'),
kwargs.get('name'),
kwargs.get('description'),
snapshot,
kwargs.get('volume_type'),
kwargs.get('metadata'),
kwargs.get('availability_zone'))
**create_kwargs)
db.ec2_volume_create(context, volume['id'])
# TODO(vish): Instance should be None at db layer instead of

View File

@ -189,6 +189,9 @@ class ExtensionManager(object):
for _alias, ext in self.sorted_ext_list:
yield ext
def is_loaded(self, alias):
return alias in self.extensions
def register(self, ext):
# Do nothing if the extension doesn't check out
if not self._check_extension(ext):

View File

@ -0,0 +1,31 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 NTT.
# Copyright (c) 2012 OpenStack, LLC.
# 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.
"""The Create Volume from Image extension."""
from nova.api.openstack import extensions
class Image_create(extensions.ExtensionDescriptor):
"""Allow creating a volume from an image in the Create Volume v1 API"""
name = "CreateVolumeExtension"
alias = "os-image-create"
namespace = "http://docs.openstack.org/volume/ext/image-create/api/v1"
updated = "2012-08-13T00:00:00+00:00"

View File

@ -0,0 +1,131 @@
# Copyright 2012 OpenStack, LLC.
#
# 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 webob
from xml.dom import minidom
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
from nova import exception
from nova import flags
from nova.openstack.common import log as logging
from nova.openstack.common.rpc import common as rpc_common
from nova import volume
FLAGS = flags.FLAGS
LOG = logging.getLogger(__name__)
def authorize(context, action_name):
action = 'volume_actions:%s' % action_name
extensions.extension_authorizer('volume', action)(context)
class VolumeToImageSerializer(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('os-volume_upload_image',
selector='os-volume_upload_image')
root.set('id')
root.set('updated_at')
root.set('status')
root.set('display_description')
root.set('size')
root.set('volume_type')
root.set('image_id')
root.set('container_format')
root.set('disk_format')
root.set('image_name')
return xmlutil.MasterTemplate(root, 1)
class VolumeToImageDeserializer(wsgi.XMLDeserializer):
"""Deserializer to handle xml-formatted requests"""
def default(self, string):
dom = minidom.parseString(string)
action_node = dom.childNodes[0]
action_name = action_node.tagName
action_data = {}
attributes = ["force", "image_name", "container_format", "disk_format"]
for attr in attributes:
if action_node.hasAttribute(attr):
action_data[attr] = action_node.getAttribute(attr)
if 'force' in action_data and action_data['force'] == 'True':
action_data['force'] = True
return {'body': {action_name: action_data}}
class VolumeActionsController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(VolumeActionsController, self).__init__(*args, **kwargs)
self.volume_api = volume.API()
@wsgi.response(202)
@wsgi.action('os-volume_upload_image')
@wsgi.serializers(xml=VolumeToImageSerializer)
@wsgi.deserializers(xml=VolumeToImageDeserializer)
def _volume_upload_image(self, req, id, body):
"""Uploads the specified volume to image service."""
context = req.environ['nova.context']
try:
params = body['os-volume_upload_image']
except (TypeError, KeyError):
msg = _("Invalid request body")
raise webob.exc.HTTPBadRequest(explanation=msg)
if not params.get("image_name"):
msg = _("No image_name was specified in request.")
raise webob.exc.HTTPBadRequest(explanation=msg)
force = params.get('force', False)
try:
volume = self.volume_api.get(context, id)
except exception.VolumeNotFound, error:
raise webob.exc.HTTPNotFound(explanation=unicode(error))
authorize(context, "upload_image")
image_metadata = {"container_format": params.get("container_format",
"bare"),
"disk_format": params.get("disk_format", "raw"),
"name": params["image_name"]}
try:
response = self.volume_api.copy_volume_to_image(context,
volume,
image_metadata,
force)
except exception.InvalidVolume, error:
raise webob.exc.HTTPBadRequest(explanation=unicode(error))
except ValueError, error:
raise webob.exc.HTTPBadRequest(explanation=unicode(error))
except rpc_common.RemoteError as error:
msg = "%(err_type)s: %(err_msg)s" % {'err_type': error.exc_type,
'err_msg': error.value}
raise webob.exc.HTTPBadRequest(explanation=msg)
return {'os-volume_upload_image': response}
class Volume_actions(extensions.ExtensionDescriptor):
"""Enable volume actions
"""
name = "VolumeActions"
alias = "os-volume-actions"
namespace = "http://docs.openstack.org/volume/ext/volume-actions/api/v1.1"
updated = "2012-05-31T00:00:00+00:00"
def get_controller_extensions(self):
controller = VolumeActionsController()
extension = extensions.ControllerExtension(self, 'volumes', controller)
return [extension]

View File

@ -25,6 +25,7 @@ from nova.api.openstack import xmlutil
from nova import exception
from nova import flags
from nova.openstack.common import log as logging
from nova import utils
from nova import volume
from nova.volume import volume_types
@ -62,17 +63,17 @@ def _translate_attachment_summary_view(_context, vol):
return d
def _translate_volume_detail_view(context, vol):
def _translate_volume_detail_view(context, vol, image_id=None):
"""Maps keys for volumes details view."""
d = _translate_volume_summary_view(context, vol)
d = _translate_volume_summary_view(context, vol, image_id)
# No additional data / lookups at the moment
return d
def _translate_volume_summary_view(context, vol):
def _translate_volume_summary_view(context, vol, image_id=None):
"""Maps keys for volumes summary view."""
d = {}
@ -98,6 +99,9 @@ def _translate_volume_summary_view(context, vol):
d['snapshot_id'] = vol['snapshot_id']
if image_id:
d['image_id'] = image_id
LOG.audit(_("vol=%s"), vol, context=context)
if vol.get('volume_metadata'):
@ -195,7 +199,7 @@ class CreateDeserializer(CommonDeserializer):
class VolumeController(wsgi.Controller):
"""The Volumes API controller for the OpenStack API."""
def __init__(self, ext_mgr=None):
def __init__(self, ext_mgr):
self.volume_api = volume.API()
self.ext_mgr = ext_mgr
super(VolumeController, self).__init__()
@ -250,6 +254,21 @@ class VolumeController(wsgi.Controller):
res = [entity_maker(context, vol) for vol in limited_list]
return {'volumes': res}
def _image_uuid_from_href(self, image_href):
# If the image href was generated by nova api, strip image_href
# down to an id.
try:
image_uuid = image_href.split('/').pop()
except (TypeError, AttributeError):
msg = _("Invalid imageRef provided.")
raise exc.HTTPBadRequest(explanation=msg)
if not utils.is_uuid_like(image_uuid):
msg = _("Invalid imageRef provided.")
raise exc.HTTPBadRequest(explanation=msg)
return image_uuid
@wsgi.serializers(xml=VolumeTemplate)
@wsgi.deserializers(xml=CreateDeserializer)
def create(self, req, body):
@ -285,6 +304,17 @@ class VolumeController(wsgi.Controller):
LOG.audit(_("Create volume of %s GB"), size, context=context)
image_href = None
image_uuid = None
if self.ext_mgr.is_loaded('os-image-create'):
image_href = volume.get('imageRef')
if snapshot_id and image_href:
msg = _("Snapshot and image cannot be specified together.")
raise exc.HTTPBadRequest(explanation=msg)
if image_href:
image_uuid = self._image_uuid_from_href(image_href)
kwargs['image_id'] = image_uuid
kwargs['availability_zone'] = volume.get('availability_zone', None)
new_volume = self.volume_api.create(context,
@ -296,7 +326,8 @@ class VolumeController(wsgi.Controller):
# TODO(vish): Instance should be None at db layer instead of
# trying to lazy load, but for now we turn it into
# a dict to avoid an error.
retval = _translate_volume_detail_view(context, dict(new_volume))
retval = _translate_volume_detail_view(context, dict(new_volume),
image_uuid)
result = {'volume': retval}

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

@ -542,6 +542,18 @@ def stub_volume_create(self, context, size, name, description, snapshot,
return vol
def stub_volume_create_from_image(self, context, size, name, description,
snapshot, volume_type, metadata,
availability_zone):
vol = stub_volume('1')
vol['status'] = 'creating'
vol['size'] = size
vol['display_name'] = name
vol['display_description'] = description
vol['availability_zone'] = 'nova'
return vol
def stub_volume_update(self, context, *args, **param):
pass

View File

@ -0,0 +1,162 @@
# Copyright 2012 OpenStack LLC.
#
# 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 datetime
import webob
from nova.api.openstack.volume.contrib import volume_actions
from nova import exception
from nova.openstack.common.rpc import common as rpc_common
from nova import test
from nova.tests.api.openstack import fakes
from nova.volume import api as volume_api
def stub_volume_get(self, context, volume_id):
volume = fakes.stub_volume(volume_id)
if volume_id == 5:
volume['status'] = 'in-use'
else:
volume['status'] = 'available'
return volume
def stub_upload_volume_to_image_service(self, context, volume, metadata,
force):
ret = {"id": volume['id'],
"updated_at": datetime.datetime(1, 1, 1, 1, 1, 1),
"status": 'uploading',
"display_description": volume['display_description'],
"size": volume['size'],
"volume_type": volume['volume_type'],
"image_id": 1,
"container_format": 'bare',
"disk_format": 'raw',
"image_name": 'image_name'}
return ret
class VolumeImageActionsTest(test.TestCase):
def setUp(self):
super(VolumeImageActionsTest, self).setUp()
self.controller = volume_actions.VolumeActionsController()
self.stubs.Set(volume_api.API, 'get', stub_volume_get)
def test_copy_volume_to_image(self):
self.stubs.Set(volume_api.API,
"copy_volume_to_image",
stub_upload_volume_to_image_service)
id = 1
vol = {"container_format": 'bare',
"disk_format": 'raw',
"image_name": 'image_name',
"force": True}
body = {"os-volume_upload_image": vol}
req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id)
res_dict = self.controller._volume_upload_image(req, id, body)
expected = {'os-volume_upload_image': {'id': id,
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'status': 'uploading',
'display_description': 'displaydesc',
'size': 1,
'volume_type': {'name': 'vol_type_name'},
'image_id': 1,
'container_format': 'bare',
'disk_format': 'raw',
'image_name': 'image_name'}}
self.assertDictMatch(res_dict, expected)
def test_copy_volume_to_image_volumenotfound(self):
def stub_volume_get_raise_exc(self, context, volume_id):
raise exception.VolumeNotFound(volume_id=volume_id)
self.stubs.Set(volume_api.API, 'get', stub_volume_get_raise_exc)
id = 1
vol = {"container_format": 'bare',
"disk_format": 'raw',
"image_name": 'image_name',
"force": True}
body = {"os-volume_upload_image": vol}
req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller._volume_upload_image,
req,
id,
body)
def test_copy_volume_to_image_invalidvolume(self):
def stub_upload_volume_to_image_service_raise(self, context, volume,
metadata, force):
raise exception.InvalidVolume
self.stubs.Set(volume_api.API,
"copy_volume_to_image",
stub_upload_volume_to_image_service_raise)
id = 1
vol = {"container_format": 'bare',
"disk_format": 'raw',
"image_name": 'image_name',
"force": True}
body = {"os-volume_upload_image": vol}
req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._volume_upload_image,
req,
id,
body)
def test_copy_volume_to_image_valueerror(self):
def stub_upload_volume_to_image_service_raise(self, context, volume,
metadata, force):
raise ValueError
self.stubs.Set(volume_api.API,
"copy_volume_to_image",
stub_upload_volume_to_image_service_raise)
id = 1
vol = {"container_format": 'bare',
"disk_format": 'raw',
"image_name": 'image_name',
"force": True}
body = {"os-volume_upload_image": vol}
req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._volume_upload_image,
req,
id,
body)
def test_copy_volume_to_image_remoteerror(self):
def stub_upload_volume_to_image_service_raise(self, context, volume,
metadata, force):
raise rpc_common.RemoteError
self.stubs.Set(volume_api.API,
"copy_volume_to_image",
stub_upload_volume_to_image_service_raise)
id = 1
vol = {"container_format": 'bare',
"disk_format": 'raw',
"image_name": 'image_name',
"force": True}
body = {"os-volume_upload_image": vol}
req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._volume_upload_image,
req,
id,
body)

View File

@ -44,12 +44,16 @@ def create_resource(ext_mgr):
return wsgi.Resource(FakeController(ext_mgr))
def create_volume_resource(ext_mgr):
return wsgi.Resource(FakeController(ext_mgr))
class VolumeRouterTestCase(test.TestCase):
def setUp(self):
super(VolumeRouterTestCase, self).setUp()
# NOTE(vish): versions is just returning text so, no need to stub.
self.stubs.Set(snapshots, 'create_resource', create_resource)
self.stubs.Set(volumes, 'create_resource', create_resource)
self.stubs.Set(volumes, 'create_resource', create_volume_resource)
self.app = volume.APIRouter()
def test_versions(self):

View File

@ -26,11 +26,29 @@ from nova import flags
from nova.openstack.common import timeutils
from nova import test
from nova.tests.api.openstack import fakes
from nova.tests.image import fake as fake_image
from nova.volume import api as volume_api
FLAGS = flags.FLAGS
TEST_SNAPSHOT_UUID = '00000000-0000-0000-0000-000000000001'
def stub_snapshot_get(self, context, snapshot_id):
if snapshot_id != TEST_SNAPSHOT_UUID:
raise exception.NotFound
return {
'id': snapshot_id,
'volume_id': 12,
'status': 'available',
'volume_size': 100,
'created_at': None,
'display_name': 'Default name',
'display_description': 'Default description',
}
class VolumeApiTest(test.TestCase):
def setUp(self):
@ -86,6 +104,92 @@ class VolumeApiTest(test.TestCase):
req,
body)
def test_volume_create_no_body(self):
body = {}
req = fakes.HTTPRequest.blank('/v1/volumes')
self.assertRaises(webob.exc.HTTPUnprocessableEntity,
self.controller.create,
req,
body)
def test_volume_create_with_image_id(self):
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
self.ext_mgr.extensions = {'os-image-create': 'fake'}
vol = {"size": '1',
"display_name": "Volume Test Name",
"display_description": "Volume Test Desc",
"availability_zone": "nova",
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'}
expected = {'volume': {'status': 'fakestatus',
'display_description': 'Volume Test Desc',
'availability_zone': 'nova',
'display_name': 'Volume Test Name',
'attachments': [{'device': '/',
'server_id': 'fakeuuid',
'id': '1',
'volume_id': '1'}],
'volume_type': 'vol_type_name',
'image_id': 'c905cedb-7281-47e4-8a62-f26bc5fc4c77',
'snapshot_id': None,
'metadata': {},
'id': '1',
'created_at': datetime.datetime(1999, 1, 1,
1, 1, 1),
'size': '1'}
}
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v1/volumes')
res = self.controller.create(req, body)
self.maxDiff = 4096
self.assertEqual(res.obj, expected)
def test_volume_create_with_image_id_and_snapshot_id(self):
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
self.stubs.Set(volume_api.API, "get_snapshot", stub_snapshot_get)
self.ext_mgr.extensions = {'os-image-create': 'fake'}
vol = {"size": '1',
"display_name": "Volume Test Name",
"display_description": "Volume Test Desc",
"availability_zone": "nova",
"imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77',
"snapshot_id": TEST_SNAPSHOT_UUID}
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v1/volumes')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
def test_volume_create_with_image_id_is_integer(self):
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
self.ext_mgr.extensions = {'os-image-create': 'fake'}
vol = {"size": '1',
"display_name": "Volume Test Name",
"display_description": "Volume Test Desc",
"availability_zone": "nova",
"imageRef": 1234}
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v1/volumes')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
def test_volume_create_with_image_id_not_uuid_format(self):
self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create)
self.ext_mgr.extensions = {'os-image-create': 'fake'}
vol = {"size": '1',
"display_name": "Volume Test Name",
"display_description": "Volume Test Desc",
"availability_zone": "nova",
"imageRef": '12345'}
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v1/volumes')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
req,
body)
def test_volume_list(self):
self.stubs.Set(volume_api.API, 'get_all',
fakes.stub_volume_get_all_by_project)
@ -475,7 +579,9 @@ class VolumesUnprocessableEntityTestCase(test.TestCase):
def setUp(self):
super(VolumesUnprocessableEntityTestCase, self).setUp()
self.controller = volumes.VolumeController()
self.ext_mgr = extensions.ExtensionManager()
self.ext_mgr.extensions = {}
self.controller = volumes.VolumeController(self.ext_mgr)
def _unprocessable_volume_create(self, body):
req = fakes.HTTPRequest.blank('/v2/fake/volumes')

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()

View File

@ -25,6 +25,7 @@ import functools
from nova.db import base
from nova import exception
from nova import flags
from nova.image import glance
from nova.openstack.common import cfg
from nova.openstack.common import log as logging
from nova.openstack.common import rpc
@ -42,6 +43,7 @@ FLAGS.register_opt(volume_host_opt)
flags.DECLARE('storage_availability_zone', 'nova.volume.manager')
LOG = logging.getLogger(__name__)
GB = 1048576 * 1024
QUOTAS = quota.QUOTAS
@ -73,12 +75,15 @@ def check_policy(context, action, target_obj=None):
class API(base.Base):
"""API for interacting with the volume manager."""
def __init__(self, **kwargs):
def __init__(self, image_service=None, **kwargs):
self.image_service = (image_service or
glance.get_default_image_service())
self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
super(API, self).__init__(**kwargs)
def create(self, context, size, name, description, snapshot=None,
volume_type=None, metadata=None, availability_zone=None):
image_id=None, volume_type=None, metadata=None,
availability_zone=None):
check_policy(context, 'create')
if snapshot is not None:
if snapshot['status'] != "available":
@ -129,6 +134,15 @@ class API(base.Base):
% locals())
raise exception.VolumeLimitExceeded(allowed=quotas['volumes'])
if image_id:
# check image existence
image_meta = self.image_service.show(context, image_id)
image_size_in_gb = (int(image_meta['size']) + GB - 1) / GB
#check image size is not larger than volume size.
if image_size_in_gb > size:
msg = _('Size of specified image is larger than volume size.')
raise exception.InvalidInput(reason=msg)
if availability_zone is None:
availability_zone = FLAGS.storage_availability_zone
@ -150,14 +164,13 @@ class API(base.Base):
'volume_type_id': volume_type_id,
'metadata': metadata,
}
volume = self.db.volume_create(context, options)
self._cast_create_volume(context, volume['id'],
snapshot_id, reservations)
snapshot_id, image_id, reservations)
return volume
def _cast_create_volume(self, context, volume_id,
snapshot_id, reservations):
snapshot_id, image_id, reservations):
# NOTE(Rongze Zhu): It is a simple solution for bug 1008866
# If snapshot_id is set, make the call create volume directly to
@ -176,10 +189,12 @@ class API(base.Base):
{"method": "create_volume",
"args": {"volume_id": volume_id,
"snapshot_id": snapshot_id,
"reservations": reservations}})
"reservations": reservations,
"image_id": image_id}})
else:
self.scheduler_rpcapi.create_volume(
context, volume_id, snapshot_id, reservations)
context, volume_id, snapshot_id, image_id, reservations)
@wrap_check_policy
def delete(self, context, volume, force=False):
@ -206,6 +221,10 @@ class API(base.Base):
{"method": "delete_volume",
"args": {"volume_id": volume_id}})
@wrap_check_policy
def update(self, context, volume, fields):
self.db.volume_update(context, volume['id'], fields)
def get(self, context, volume_id):
rv = self.db.volume_get(context, volume_id)
volume = dict(rv.iteritems())
@ -437,3 +456,40 @@ class API(base.Base):
if i['key'] == key:
return i['value']
return None
def _check_volume_availability(self, context, volume, force):
"""Check if the volume can be used."""
if volume['status'] not in ['available', 'in-use']:
msg = _('Volume status must be available/in-use.')
raise exception.InvalidVolume(reason=msg)
if not force and 'in-use' == volume['status']:
msg = _('Volume status is in-use.')
raise exception.InvalidVolume(reason=msg)
@wrap_check_policy
def copy_volume_to_image(self, context, volume, metadata, force):
"""Create a new image from the specified volume."""
self._check_volume_availability(context, volume, force)
recv_metadata = self.image_service.create(context, metadata)
self.update(context, volume, {'status': 'uploading'})
rpc.cast(context,
rpc.queue_get_for(context,
FLAGS.volume_topic,
volume['host']),
{"method": "copy_volume_to_image",
"args": {"volume_id": volume['id'],
"image_id": recv_metadata['id']}})
response = {"id": volume['id'],
"updated_at": volume['updated_at'],
"status": 'uploading',
"display_description": volume['display_description'],
"size": volume['size'],
"volume_type": volume['volume_type'],
"image_id": recv_metadata['id'],
"container_format": recv_metadata['container_format'],
"disk_format": recv_metadata['disk_format'],
"image_name": recv_metadata.get('name', None)
}
return response

View File

@ -197,21 +197,25 @@ class API(base.Base):
volumes.terminate_connection(volume['id'], connector)
def create(self, context, size, name, description, snapshot=None,
volume_type=None, metadata=None, availability_zone=None):
image_id=None, volume_type=None, metadata=None,
availability_zone=None):
if snapshot is not None:
snapshot_id = snapshot['id']
else:
snapshot_id = None
item = cinderclient(context).volumes.create(size,
snapshot_id,
name,
description,
volume_type,
context.user_id,
context.project_id,
availability_zone,
metadata)
kwargs = dict(snapshot_id=snapshot_id,
display_name=name,
display_description=description,
volume_type=volume_type,
user_id=context.user_id,
project_id=context.project_id,
availability_zone=availability_zone,
metadata=metadata,
imageRef=image_id)
item = cinderclient(context).volumes.create(size, **kwargs)
return _untranslate_volume_summary_view(context, item)

View File

@ -261,6 +261,14 @@ class VolumeDriver(object):
"""Any initialization the volume driver does while starting"""
pass
def copy_image_to_volume(self, context, volume, image_service, image_id):
"""Fetch the image from image_service and write it to the volume."""
raise NotImplementedError()
def copy_volume_to_image(self, context, volume, image_service, image_id):
"""Copy the volume to the specified image."""
raise NotImplementedError()
class ISCSIDriver(VolumeDriver):
"""Executes commands relating to ISCSI volumes.
@ -541,6 +549,20 @@ class ISCSIDriver(VolumeDriver):
"id:%(volume_id)s.") % locals())
raise
def copy_image_to_volume(self, context, volume, image_service, image_id):
"""Fetch the image from image_service and write it to the volume."""
volume_path = self.local_path(volume)
with utils.temporary_chown(volume_path):
with utils.file_open(volume_path, "wb") as image_file:
image_service.download(context, image_id, image_file)
def copy_volume_to_image(self, context, volume, image_service, image_id):
"""Copy the volume to the specified image."""
volume_path = self.local_path(volume)
with utils.temporary_chown(volume_path):
with utils.file_open(volume_path) as volume_file:
image_service.update(context, image_id, {}, volume_file)
class FakeISCSIDriver(ISCSIDriver):
"""Logs calls instead of executing."""

View File

@ -41,6 +41,7 @@ intact.
from nova import context
from nova import exception
from nova import flags
from nova.image import glance
from nova import manager
from nova.openstack.common import cfg
from nova.openstack.common import excutils
@ -112,7 +113,7 @@ class VolumeManager(manager.SchedulerDependentManager):
self.delete_volume(ctxt, volume['id'])
def create_volume(self, context, volume_id, snapshot_id=None,
reservations=None):
image_id=None, reservations=None):
"""Creates and exports the volume."""
context = context.elevated()
volume_ref = self.db.volume_get(context, volume_id)
@ -126,6 +127,11 @@ class VolumeManager(manager.SchedulerDependentManager):
# before passing it to the driver.
volume_ref['host'] = self.host
if image_id:
status = 'downloading'
else:
status = 'available'
try:
vol_name = volume_ref['name']
vol_size = volume_ref['size']
@ -158,11 +164,15 @@ class VolumeManager(manager.SchedulerDependentManager):
now = timeutils.utcnow()
volume_ref = self.db.volume_update(context,
volume_ref['id'], {'status': 'available',
volume_ref['id'], {'status': status,
'launched_at': now})
LOG.debug(_("volume %s: created successfully"), volume_ref['name'])
self._reset_stats()
self._notify_about_volume_usage(context, volume_ref, "create.end")
if image_id:
#copy the image onto the volume.
self._copy_image_to_volume(context, volume_ref, image_id)
return volume_id
def delete_volume(self, context, volume_id):
@ -174,7 +184,7 @@ class VolumeManager(manager.SchedulerDependentManager):
raise exception.VolumeAttached(volume_id=volume_id)
if volume_ref['host'] != self.host:
raise exception.InvalidVolume(
reason=_("Volume is not local to this node"))
reason=_("Volume is not local to this node"))
self._notify_about_volume_usage(context, volume_ref, "delete.start")
self._reset_stats()
@ -183,7 +193,7 @@ class VolumeManager(manager.SchedulerDependentManager):
self.driver.remove_export(context, volume_ref)
LOG.debug(_("volume %s: deleting"), volume_ref['name'])
self.driver.delete_volume(volume_ref)
except exception.VolumeIsBusy, e:
except exception.VolumeIsBusy:
LOG.debug(_("volume %s: volume is busy"), volume_ref['name'])
self.driver.ensure_export(context, volume_ref)
self.db.volume_update(context, volume_ref['id'],
@ -298,6 +308,47 @@ class VolumeManager(manager.SchedulerDependentManager):
self.db.volume_detached(context, volume_id)
def _copy_image_to_volume(self, context, volume, image_id):
"""Downloads Glance image to the specified volume. """
volume_id = volume['id']
payload = {'volume_id': volume_id, 'image_id': image_id}
try:
image_service, image_id = glance.get_remote_image_service(context,
image_id)
self.driver.copy_image_to_volume(context, volume, image_service,
image_id)
LOG.debug(_("Downloaded image %(image_id)s to %(volume_id)s "
"successfully") % locals())
self.db.volume_update(context, volume_id,
{'status': 'available'})
except Exception, error:
with excutils.save_and_reraise_exception():
payload['message'] = unicode(error)
self.db.volume_update(context, volume_id, {'status': 'error'})
def copy_volume_to_image(self, context, volume_id, image_id):
"""Uploads the specified volume to Glance."""
payload = {'volume_id': volume_id, 'image_id': image_id}
try:
volume = self.db.volume_get(context, volume_id)
self.driver.ensure_export(context.elevated(), volume)
image_service, image_id = glance.get_remote_image_service(context,
image_id)
self.driver.copy_volume_to_image(context, volume, image_service,
image_id)
LOG.debug(_("Uploaded volume %(volume_id)s to "
"image (%(image_id)s) successfully") % locals())
except Exception, error:
with excutils.save_and_reraise_exception():
payload['message'] = unicode(error)
finally:
if volume['instance_uuid'] is None:
self.db.volume_update(context, volume_id,
{'status': 'available'})
else:
self.db.volume_update(context, volume_id,
{'status': 'in-use'})
def initialize_connection(self, context, volume_id, connector):
"""Prepare volume for connection from host represented by connector.