Adds disk config

Change-Id: If3e1765b659ead77f9cdaaa86ee8478a82bf67c0
This commit is contained in:
Matt Dietz 2011-09-23 15:36:50 -05:00
parent 4e94ec1a0a
commit 774b5aaa17
9 changed files with 377 additions and 12 deletions

View File

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

View File

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

View File

@ -241,6 +241,7 @@ class Instance(BASE, NovaBase):
access_ip_v4 = Column(String(255)) access_ip_v4 = Column(String(255))
access_ip_v6 = Column(String(255)) access_ip_v6 = Column(String(255))
managed_disk = Column(Boolean())
progress = Column(Integer) progress = Column(Integer)

View File

@ -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

View File

@ -102,6 +102,7 @@ class ExtensionControllerTest(test.TestCase):
"VirtualInterfaces", "VirtualInterfaces",
"Volumes", "Volumes",
"VolumeTypes", "VolumeTypes",
"DiskConfig",
] ]
self.ext_list.sort() self.ext_list.sort()

View File

@ -298,6 +298,9 @@ class FakeSessionForMigrationTests(fake.SessionBase):
def VM_set_name_label(self, *args): def VM_set_name_label(self, *args):
pass pass
def VDI_set_name_label(self, session_ref, vdi_ref, name_label):
pass
def stub_out_migration_methods(stubs): def stub_out_migration_methods(stubs):
def fake_create_snapshot(self, instance): def fake_create_snapshot(self, instance):

View File

@ -313,6 +313,11 @@ class VMHelper(HelperBase):
% locals()) % locals())
return vdi_ref 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 @classmethod
def get_vdi_for_vm_safely(cls, session, vm_ref): def get_vdi_for_vm_safely(cls, session, vm_ref):
"""Retrieves the primary VDI for a VM""" """Retrieves the primary VDI for a VM"""
@ -370,7 +375,8 @@ class VMHelper(HelperBase):
return os.path.join(FLAGS.xenapi_sr_base_path, sr_uuid) return os.path.join(FLAGS.xenapi_sr_base_path, sr_uuid)
@classmethod @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 """ Requests that the Glance plugin bundle the specified VDIs and
push them into Glance using the specified human-friendly name. push them into Glance using the specified human-friendly name.
""" """
@ -388,7 +394,8 @@ class VMHelper(HelperBase):
'glance_port': glance_port, 'glance_port': glance_port,
'sr_path': cls.get_sr_path(session), 'sr_path': cls.get_sr_path(session),
'os_type': os_type, '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)} kwargs = {'params': pickle.dumps(params)}
task = session.async_call_plugin('glance', 'upload_vhd', kwargs) task = session.async_call_plugin('glance', 'upload_vhd', kwargs)
@ -471,7 +478,7 @@ class VMHelper(HelperBase):
# Set the name-label to ease debugging # Set the name-label to ease debugging
vdi_ref = session.get_xenapi().VDI.get_by_uuid(os_vdi_uuid) 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) session.get_xenapi().VDI.set_name_label(vdi_ref, primary_name_label)
cls._check_vdi_size(context, session, instance, os_vdi_uuid) 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, " _("Kernel/Ramdisk image is too large: %(vdi_size)d bytes, "
"max %(max_size)d bytes") % locals()) "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) vdi_ref = cls.create_vdi(session, sr_ref, name_label, vdi_size, False)
# From this point we have a VDI on Xen host; # From this point we have a VDI on Xen host;
# If anything goes wrong, we need to remember its uuid. # 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) 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): def _mount_filesystem(dev_path, dir):
"""mounts the device specified by dev_path in dir""" """mounts the device specified by dev_path in dir"""
try: try:

View File

@ -549,12 +549,16 @@ class VMOps(object):
""" """
template_vm_ref = None template_vm_ref = None
options = None
if instance['managed_disk']:
options = {'managed_disk': instance['managed_disk']}
try: try:
template_vm_ref, template_vdi_uuids =\ template_vm_ref, template_vdi_uuids =\
self._create_snapshot(instance) self._create_snapshot(instance)
# call plugin to ship snapshot off to glance # call plugin to ship snapshot off to glance
VMHelper.upload_image(context, VMHelper.upload_image(context,
self._session, instance, template_vdi_uuids, image_id) self._session, instance, template_vdi_uuids, image_id,
options)
finally: finally:
if template_vm_ref: if template_vm_ref:
self._destroy(instance, 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 # Now we rescan the SR so we find the VHDs
VMHelper.scan_default_sr(self._session) 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 return new_cow_uuid
def resize_instance(self, instance, vdi_uuid): def resize_instance(self, instance, vdi_uuid):

View File

@ -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, 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 Create a tarball of the image and then stream that into Glance
using chunked-transfer-encoded HTTP. 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-container-format': 'ovf',
'x-image-meta-property-os-type': os_type} '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 we have an auth_token, set an x-auth-token header
if auth_token: if auth_token:
ovf_headers['x-auth-token'] = auth_token ovf_headers['x-auth-token'] = auth_token
@ -424,12 +427,13 @@ def upload_vhd(session, args):
sr_path = params["sr_path"] sr_path = params["sr_path"]
os_type = params["os_type"] os_type = params["os_type"]
auth_token = params["auth_token"] auth_token = params["auth_token"]
options = params["options"]
staging_path = _make_staging_area(sr_path) staging_path = _make_staging_area(sr_path)
try: try:
_prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids) _prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids)
_upload_tarball(staging_path, image_id, glance_host, glance_port, _upload_tarball(staging_path, image_id, glance_host, glance_port,
os_type, auth_token) os_type, auth_token, options)
finally: finally:
_cleanup_staging_area(staging_path) _cleanup_staging_area(staging_path)