summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatt Dietz <matt.dietz@rackspace.com>2011-09-23 15:36:50 -0500
committerMatt Dietz <matt.dietz@rackspace.com>2011-09-23 15:38:36 -0500
commit774b5aaa173fa04675be5252c5d47f67a07347ac (patch)
tree60416f512ce6c2c873b79b7c00e95875e5c36864
parent4e94ec1a0a566b66f09b734e6ffe964b4b3b4bee (diff)
Adds disk config
Change-Id: If3e1765b659ead77f9cdaaa86ee8478a82bf67c0
-rw-r--r--nova/api/openstack/contrib/diskconfig.py150
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/050_add_disk_config_to_instances.py39
-rw-r--r--nova/db/sqlalchemy/models.py1
-rw-r--r--nova/tests/api/openstack/contrib/test_diskconfig.py156
-rw-r--r--nova/tests/api/openstack/test_extensions.py1
-rw-r--r--nova/tests/xenapi/stubs.py3
-rw-r--r--nova/virt/xenapi/vm_utils.py20
-rw-r--r--nova/virt/xenapi/vmops.py11
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/glance8
9 files changed, 377 insertions, 12 deletions
diff --git a/nova/api/openstack/contrib/diskconfig.py b/nova/api/openstack/contrib/diskconfig.py
new file mode 100644
index 000000000..5173050fd
--- /dev/null
+++ b/nova/api/openstack/contrib/diskconfig.py
@@ -0,0 +1,150 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 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 json
+
+from webob import exc
+import webob
+
+from nova import compute
+from nova import exception
+import nova.image
+from nova import log as logging
+from nova import network
+from nova import rpc
+from nova.api.openstack import faults
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+
+LOG = logging.getLogger('nova.api.openstack.contrib.disk_config')
+
+
+class DiskConfigController(object):
+ def __init__(self):
+ self.compute_api = compute.API()
+
+ def _return_dict(self, server_id, managed_disk):
+ return {'server': {'id': server_id,
+ 'managed_disk': managed_disk}}
+
+ def index(self, req, server_id):
+ context = req.environ['nova.context']
+ try:
+ server = self.compute_api.routing_get(context, server_id)
+ except exception.NotFound:
+ explanation = _("Server not found.")
+ return faults.Fault(exc.HTTPNotFound(explanation=explanation))
+ managed_disk = server['managed_disk'] or False
+ return self._return_dict(server_id, managed_disk)
+
+ def update(self, req, server_id, body=None):
+ if not body:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ context = req.environ['nova.context']
+ try:
+ server = self.compute_api.routing_get(context, server_id)
+ except exception.NotFound:
+ explanation = _("Server not found.")
+ return faults.Fault(exc.HTTPNotFound(explanation=explanation))
+
+ managed_disk = str(body['server'].get('managed_disk', False)).lower()
+ managed_disk = managed_disk == 'true' or False
+ self.compute_api.update(context, server_id, managed_disk=managed_disk)
+
+ return self._return_dict(server_id, managed_disk)
+
+
+class ImageDiskConfigController(object):
+ def __init__(self, image_service=None):
+ self.compute_api = compute.API()
+ self._image_service = image_service or \
+ nova.image.get_default_image_service()
+
+ def _return_dict(self, image_id, managed_disk):
+ return {'image': {'id': image_id,
+ 'managed_disk': managed_disk}}
+
+ def index(self, req, image_id):
+ context = req.environ['nova.context']
+ try:
+ image = self._image_service.show(context, image_id)
+ except (exception.NotFound, exception.InvalidImageRef):
+ explanation = _("Image not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+ image_properties = image.get('properties', None)
+ if image_properties:
+ managed_disk = image_properties.get('managed_disk', False)
+
+ return self._return_dict(image_id, managed_disk)
+
+
+class Diskconfig(extensions.ExtensionDescriptor):
+ def __init__(self):
+ super(Diskconfig, self).__init__()
+
+ def get_name(self):
+ return "DiskConfig"
+
+ def get_alias(self):
+ return "OS-DCFG"
+
+ def get_description(self):
+ return "Disk Configuration support"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/disk_config/api/v1.1"
+
+ def get_updated(self):
+ return "2011-08-31T00:00:00+00:00"
+
+ def _server_extension_controller(self):
+ metadata = {
+ "attributes": {
+ 'managed_disk': ["server_id", "enabled"]}}
+
+ body_serializers = {
+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
+ xmlns=wsgi.XMLNS_V11)}
+ serializer = wsgi.ResponseSerializer(body_serializers, None)
+ res = extensions.ResourceExtension(
+ 'os-disk-config',
+ controller=DiskConfigController(),
+ collection_actions={'update': 'PUT'},
+ parent=dict(member_name='server', collection_name='servers'),
+ serializer=serializer)
+ return res
+
+ def _image_extension_controller(self):
+ resources = []
+ metadata = {
+ "attributes": {
+ 'managed_disk': ["image_id", "enabled"]}}
+
+ body_serializers = {
+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
+ xmlns=wsgi.XMLNS_V11)}
+ serializer = wsgi.ResponseSerializer(body_serializers, None)
+ res = extensions.ResourceExtension(
+ 'os-disk-config',
+ controller=ImageDiskConfigController(),
+ collection_actions={'update': 'PUT'},
+ parent=dict(member_name='image', collection_name='images'),
+ serializer=serializer)
+ return res
+
+ def get_resources(self):
+ return [self._server_extension_controller(),
+ self._image_extension_controller()]
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/050_add_disk_config_to_instances.py b/nova/db/sqlalchemy/migrate_repo/versions/050_add_disk_config_to_instances.py
new file mode 100644
index 000000000..72e49f2ad
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/050_add_disk_config_to_instances.py
@@ -0,0 +1,39 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2011 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.
+
+from sqlalchemy import Column, Integer, MetaData, Table, Boolean
+
+meta = MetaData()
+
+# temporary table for creating the new columns
+
+instances = Table("instances", meta,
+ Column("id", Integer(), primary_key=True, nullable=False))
+
+# The new column
+
+managed_disk = Column("managed_disk", Boolean(create_constraint=False,
+ name=None))
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instances.create_column(managed_disk)
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instances.drop_column(managed_disk)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 2261a1d09..2d9340777 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -241,6 +241,7 @@ class Instance(BASE, NovaBase):
access_ip_v4 = Column(String(255))
access_ip_v6 = Column(String(255))
+ managed_disk = Column(Boolean())
progress = Column(Integer)
diff --git a/nova/tests/api/openstack/contrib/test_diskconfig.py b/nova/tests/api/openstack/contrib/test_diskconfig.py
new file mode 100644
index 000000000..b6843b49c
--- /dev/null
+++ b/nova/tests/api/openstack/contrib/test_diskconfig.py
@@ -0,0 +1,156 @@
+# Copyright 2011 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.
+
+import json
+import webob
+
+from nova import compute
+from nova import exception
+from nova import image
+from nova import test
+from nova.api.openstack.contrib.diskconfig import DiskConfigController
+from nova.api.openstack.contrib.diskconfig import ImageDiskConfigController
+from nova.tests.api.openstack import fakes
+
+
+class DiskConfigTest(test.TestCase):
+
+ def test_retrieve_disk_config(self):
+ def fake_compute_get(*args, **kwargs):
+ return {'managed_disk': True}
+
+ self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get)
+ req = webob.Request.blank('/v1.1/openstack/servers/50/os-disk-config')
+ req.headers['Accept'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['managed_disk'], True)
+ self.assertEqual(int(body['server']['id']), 50)
+
+ def test_set_disk_config(self):
+ def fake_compute_get(*args, **kwargs):
+ return {'managed_disk': 'True'}
+
+ def fake_compute_update(*args, **kwargs):
+ return {'managed_disk': 'False'}
+
+ self.stubs.Set(compute.api.API, 'update', fake_compute_update)
+ self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get)
+
+ req = webob.Request.blank('/v1.1/openstack/servers/50/os-disk-config')
+ req.method = 'PUT'
+ req.headers['Accept'] = 'application/json'
+ req.headers['Content-Type'] = 'application/json'
+ req.body = json.dumps({'server': {'managed_disk': False}})
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['managed_disk'], False)
+ self.assertEqual(int(body['server']['id']), 50)
+
+ def test_retrieve_disk_config_bad_server_fails(self):
+ def fake_compute_get(*args, **kwargs):
+ raise exception.NotFound()
+
+ self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get)
+ req = webob.Request.blank('/v1.1/openstack/servers/50/os-disk-config')
+ req.headers['Accept'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
+
+ def test_set_disk_config_bad_server_fails(self):
+ self.called = False
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.NotFound()
+
+ def fake_compute_update(*args, **kwargs):
+ self.called = True
+
+ self.stubs.Set(compute.api.API, 'update', fake_compute_update)
+ self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get)
+
+ req = webob.Request.blank('/v1.1/openstack/servers/50/os-disk-config')
+ req.method = 'PUT'
+ req.headers['Accept'] = 'application/json'
+ req.headers['Content-Type'] = 'application/json'
+ req.body = json.dumps({'server': {'managed_disk': False}})
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
+ self.assertEqual(self.called, False)
+
+
+class ImageDiskConfigTest(test.TestCase):
+
+ NOW_GLANCE_FORMAT = "2010-10-11T10:30:22"
+ NOW_API_FORMAT = "2010-10-11T10:30:22Z"
+
+ def test_image_get_disk_config(self):
+ self.flags(image_service='nova.image.glance.GlanceImageService')
+ fakes.stub_out_glance(self.stubs)
+
+ def fake_image_service_show(*args, **kwargs):
+ return {'properties': {'managed_disk': True}}
+
+ self.stubs.Set(image.glance.GlanceImageService, 'show',
+ fake_image_service_show)
+
+ req = webob.Request.blank('/v1.1/openstack/images/10/os-disk-config')
+ req.headers['Accept'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+
+ body = json.loads(res.body)
+
+ self.assertEqual(body['image']['managed_disk'], True)
+ self.assertEqual(int(body['image']['id']), 10)
+
+ def test_image_get_disk_config_no_image_fails(self):
+ self.flags(image_service='nova.image.glance.GlanceImageService')
+ fakes.stub_out_glance(self.stubs)
+
+ def fake_image_service_show(*args, **kwargs):
+ raise exception.NotFound()
+
+ self.stubs.Set(image.glance.GlanceImageService, 'show',
+ fake_image_service_show)
+
+ req = webob.Request.blank('/v1.1/openstack/images/10/os-disk-config')
+ req.headers['Accept'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
+
+ @classmethod
+ def _make_image_fixtures(cls):
+ image_id = 123
+ base_attrs = {'created_at': cls.NOW_GLANCE_FORMAT,
+ 'updated_at': cls.NOW_GLANCE_FORMAT,
+ 'deleted_at': None,
+ 'deleted': False}
+
+ fixtures = []
+
+ def add_fixture(**kwargs):
+ kwargs.update(base_attrs)
+ fixtures.append(kwargs)
+
+ # Public image
+ add_fixture(id=1, name='snapshot', is_public=False,
+ status='active', properties={})
+
+ return fixtures
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index ca36523e4..a5c6fe65a 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -102,6 +102,7 @@ class ExtensionControllerTest(test.TestCase):
"VirtualInterfaces",
"Volumes",
"VolumeTypes",
+ "DiskConfig",
]
self.ext_list.sort()
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
index aee279920..3b3d494ba 100644
--- a/nova/tests/xenapi/stubs.py
+++ b/nova/tests/xenapi/stubs.py
@@ -298,6 +298,9 @@ class FakeSessionForMigrationTests(fake.SessionBase):
def VM_set_name_label(self, *args):
pass
+ def VDI_set_name_label(self, session_ref, vdi_ref, name_label):
+ pass
+
def stub_out_migration_methods(stubs):
def fake_create_snapshot(self, instance):
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 5778ca1c2..51f102689 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -314,6 +314,11 @@ class VMHelper(HelperBase):
return vdi_ref
@classmethod
+ def set_vdi_name_label(cls, session, vdi_uuid, name_label):
+ vdi_ref = session.get_xenapi().VDI.get_by_uuid(vdi_uuid)
+ session.get_xenapi().VDI.set_name_label(vdi_ref, name_label)
+
+ @classmethod
def get_vdi_for_vm_safely(cls, session, vm_ref):
"""Retrieves the primary VDI for a VM"""
vbd_refs = session.get_xenapi().VM.get_VBDs(vm_ref)
@@ -370,7 +375,8 @@ class VMHelper(HelperBase):
return os.path.join(FLAGS.xenapi_sr_base_path, sr_uuid)
@classmethod
- def upload_image(cls, context, session, instance, vdi_uuids, image_id):
+ def upload_image(cls, context, session, instance, vdi_uuids, image_id,
+ options=None):
""" Requests that the Glance plugin bundle the specified VDIs and
push them into Glance using the specified human-friendly name.
"""
@@ -388,7 +394,8 @@ class VMHelper(HelperBase):
'glance_port': glance_port,
'sr_path': cls.get_sr_path(session),
'os_type': os_type,
- 'auth_token': getattr(context, 'auth_token', None)}
+ 'auth_token': getattr(context, 'auth_token', None),
+ 'options': options}
kwargs = {'params': pickle.dumps(params)}
task = session.async_call_plugin('glance', 'upload_vhd', kwargs)
@@ -471,7 +478,7 @@ class VMHelper(HelperBase):
# Set the name-label to ease debugging
vdi_ref = session.get_xenapi().VDI.get_by_uuid(os_vdi_uuid)
- primary_name_label = get_name_label_for_image(image)
+ primary_name_label = instance.name
session.get_xenapi().VDI.set_name_label(vdi_ref, primary_name_label)
cls._check_vdi_size(context, session, instance, os_vdi_uuid)
@@ -559,7 +566,7 @@ class VMHelper(HelperBase):
_("Kernel/Ramdisk image is too large: %(vdi_size)d bytes, "
"max %(max_size)d bytes") % locals())
- name_label = get_name_label_for_image(image)
+ name_label = instance.name
vdi_ref = cls.create_vdi(session, sr_ref, name_label, vdi_size, False)
# From this point we have a VDI on Xen host;
# If anything goes wrong, we need to remember its uuid.
@@ -1156,11 +1163,6 @@ def _write_partition(virtual_size, dev):
LOG.debug(_('Writing partition table %s done.'), dest)
-def get_name_label_for_image(image):
- # TODO(sirp): This should eventually be the URI for the Glance image
- return _('Glance image %s') % image
-
-
def _mount_filesystem(dev_path, dir):
"""mounts the device specified by dev_path in dir"""
try:
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 1dfa5abd1..bf4481d69 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -549,12 +549,16 @@ class VMOps(object):
"""
template_vm_ref = None
+ options = None
+ if instance['managed_disk']:
+ options = {'managed_disk': instance['managed_disk']}
try:
template_vm_ref, template_vdi_uuids =\
self._create_snapshot(instance)
# call plugin to ship snapshot off to glance
VMHelper.upload_image(context,
- self._session, instance, template_vdi_uuids, image_id)
+ self._session, instance, template_vdi_uuids, image_id,
+ options)
finally:
if template_vm_ref:
self._destroy(instance, template_vm_ref,
@@ -697,6 +701,11 @@ class VMOps(object):
# Now we rescan the SR so we find the VHDs
VMHelper.scan_default_sr(self._session)
+ # Set name-label so we can find if we need to clean up a failed
+ # migration
+ VMHelper.set_vdi_name_label(self._session, new_cow_uuid,
+ instance.name)
+
return new_cow_uuid
def resize_instance(self, instance, vdi_uuid):
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
index 950b78707..e7bf7e5a8 100755
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance
@@ -246,7 +246,7 @@ def _prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids):
def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type,
- auth_token):
+ auth_token, options):
"""
Create a tarball of the image and then stream that into Glance
using chunked-transfer-encoded HTTP.
@@ -293,6 +293,9 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type,
'x-image-meta-container-format': 'ovf',
'x-image-meta-property-os-type': os_type}
+ if options.get('managed_disk'):
+ headers['x-image-meta-property-managed-disk'] = options['managed_disk']
+
# If we have an auth_token, set an x-auth-token header
if auth_token:
ovf_headers['x-auth-token'] = auth_token
@@ -424,12 +427,13 @@ def upload_vhd(session, args):
sr_path = params["sr_path"]
os_type = params["os_type"]
auth_token = params["auth_token"]
+ options = params["options"]
staging_path = _make_staging_area(sr_path)
try:
_prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids)
_upload_tarball(staging_path, image_id, glance_host, glance_port,
- os_type, auth_token)
+ os_type, auth_token, options)
finally:
_cleanup_staging_area(staging_path)