Adds disk config
Change-Id: If3e1765b659ead77f9cdaaa86ee8478a82bf67c0
This commit is contained in:
parent
4e94ec1a0a
commit
774b5aaa17
150
nova/api/openstack/contrib/diskconfig.py
Normal file
150
nova/api/openstack/contrib/diskconfig.py
Normal 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()]
|
@ -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)
|
@ -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)
|
||||
|
||||
|
||||
|
156
nova/tests/api/openstack/contrib/test_diskconfig.py
Normal file
156
nova/tests/api/openstack/contrib/test_diskconfig.py
Normal 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
|
@ -102,6 +102,7 @@ class ExtensionControllerTest(test.TestCase):
|
||||
"VirtualInterfaces",
|
||||
"Volumes",
|
||||
"VolumeTypes",
|
||||
"DiskConfig",
|
||||
]
|
||||
self.ext_list.sort()
|
||||
|
||||
|
@ -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):
|
||||
|
@ -313,6 +313,11 @@ class VMHelper(HelperBase):
|
||||
% locals())
|
||||
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"""
|
||||
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user