xenapi: Add support for different image upload drivers

This change refactors image upload code for xenapi and adds the
xenapi_image_upload_handler config option.

Preparation for bp nova-direct-image-upload
DocImpact

Change-Id: Icd4e7ef135beddca9ddcc4a880c09e5a1702a93c
This commit is contained in:
Alex Meade
2012-12-10 15:17:58 -05:00
parent 0364b173f9
commit c9af27c230
7 changed files with 165 additions and 85 deletions

View File

@@ -19,7 +19,6 @@
import ast import ast
import base64 import base64
import contextlib import contextlib
import cPickle as pickle
import functools import functools
import os import os
import re import re
@@ -48,6 +47,7 @@ from nova.virt.xenapi import agent
from nova.virt.xenapi import driver as xenapi_conn from nova.virt.xenapi import driver as xenapi_conn
from nova.virt.xenapi import fake as xenapi_fake from nova.virt.xenapi import fake as xenapi_fake
from nova.virt.xenapi import host from nova.virt.xenapi import host
from nova.virt.xenapi.imageupload import glance
from nova.virt.xenapi import pool from nova.virt.xenapi import pool
from nova.virt.xenapi import pool_states from nova.virt.xenapi import pool_states
from nova.virt.xenapi import vm_utils from nova.virt.xenapi import vm_utils
@@ -431,15 +431,29 @@ class XenAPIVMTestCase(stubs.XenAPITestBase):
{'task_state': task_states.IMAGE_UPLOADING, {'task_state': task_states.IMAGE_UPLOADING,
'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}]
func_call_matcher = matchers.FunctionCallMatcher(expected_calls) func_call_matcher = matchers.FunctionCallMatcher(expected_calls)
image_id = "my_snapshot_id"
stubs.stubout_instance_snapshot(self.stubs) stubs.stubout_instance_snapshot(self.stubs)
stubs.stubout_is_snapshot(self.stubs) stubs.stubout_is_snapshot(self.stubs)
# Stubbing out firewall driver as previous stub sets alters # Stubbing out firewall driver as previous stub sets alters
# xml rpc result parsing # xml rpc result parsing
stubs.stubout_firewall_driver(self.stubs, self.conn) stubs.stubout_firewall_driver(self.stubs, self.conn)
instance = self._create_instance() instance = self._create_instance()
image_id = "my_snapshot_id" self.fake_upload_called = False
def fake_image_upload(_self, ctx, session, inst, vdi_uuids,
img_id):
self.fake_upload_called = True
self.assertEqual(ctx, self.context)
self.assertEqual(inst, instance)
self.assertTrue(isinstance(vdi_uuids, list))
self.assertEqual(img_id, image_id)
self.stubs.Set(glance.GlanceStore, 'upload_image',
fake_image_upload)
self.conn.snapshot(self.context, instance, image_id, self.conn.snapshot(self.context, instance, image_id,
func_call_matcher.call) func_call_matcher.call)
@@ -469,6 +483,8 @@ class XenAPIVMTestCase(stubs.XenAPITestBase):
name_label = vdi_rec["name_label"] name_label = vdi_rec["name_label"]
self.assert_(not name_label.endswith('snapshot')) self.assert_(not name_label.endswith('snapshot'))
self.assertTrue(self.fake_upload_called)
def create_vm_record(self, conn, os_type, name): def create_vm_record(self, conn, os_type, name):
instances = conn.list_instances() instances = conn.list_instances()
self.assertEquals(instances, [name]) self.assertEquals(instances, [name])
@@ -2574,54 +2590,6 @@ class SwapXapiHostTestCase(test.TestCase):
"http://someserver", 'otherserver')) "http://someserver", 'otherserver'))
class VmUtilsTestCase(test.TestCase):
"""Unit tests for xenapi utils."""
def test_upload_image(self):
def fake_instance_system_metadata_get(context, uuid):
return dict(image_a=1, image_b=2, image_c='c', d='d')
def fake_get_sr_path(session):
return "foo"
class FakeInstance(dict):
def __init__(self):
super(FakeInstance, self).__init__({
'auto_disk_config': 'auto disk config',
'os_type': 'os type'})
def __missing__(self, item):
return "whatever"
class FakeSession(object):
def call_plugin(session_self, service, command, kwargs):
self.kwargs = kwargs
def call_plugin_serialized(session_self, service, command, *args,
**kwargs):
self.kwargs = kwargs
def fake_dumps(thing):
return thing
self.stubs.Set(db, "instance_system_metadata_get",
fake_instance_system_metadata_get)
self.stubs.Set(vm_utils, "get_sr_path", fake_get_sr_path)
self.stubs.Set(pickle, "dumps", fake_dumps)
ctx = context.get_admin_context()
instance = FakeInstance()
session = FakeSession()
vm_utils.upload_image(ctx, session, instance, "vmi uuids", "image id")
actual = self.kwargs['properties']
# Inheritance happens in another place, now
expected = dict(auto_disk_config='auto disk config',
os_type='os type')
self.assertEquals(expected, actual)
class XenAPILiveMigrateTestCase(stubs.XenAPITestBase): class XenAPILiveMigrateTestCase(stubs.XenAPITestBase):
"""Unit tests for live_migration.""" """Unit tests for live_migration."""
def setUp(self): def setUp(self):

View File

@@ -0,0 +1,74 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 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 mox
from nova import context
from nova import test
from nova.virt.xenapi.imageupload import glance
from nova.virt.xenapi import vm_utils
class TestGlanceStore(test.TestCase):
def setUp(self):
super(TestGlanceStore, self).setUp()
self.store = glance.GlanceStore()
self.mox = mox.Mox()
def tearDown(self):
super(TestGlanceStore, self).tearDown()
def test_upload_image(self):
glance_host = '0.1.2.3'
glance_port = 8143
glance_use_ssl = False
sr_path = '/fake/sr/path'
self.flags(glance_host=glance_host)
self.flags(glance_port=glance_port)
self.flags(glance_api_insecure=glance_use_ssl)
def fake_get_sr_path(*_args, **_kwargs):
return sr_path
self.stubs.Set(vm_utils, 'get_sr_path', fake_get_sr_path)
ctx = context.RequestContext('user', 'project', auth_token='foobar')
properties = {
'auto_disk_config': True,
'os_type': 'default',
}
image_id = 'fake_image_uuid'
vdi_uuids = ['fake_vdi_uuid']
instance = {'uuid': 'blah'}
instance.update(properties)
params = {'vdi_uuids': vdi_uuids,
'image_id': image_id,
'glance_host': glance_host,
'glance_port': glance_port,
'glance_use_ssl': glance_use_ssl,
'sr_path': sr_path,
'auth_token': 'foobar',
'properties': properties}
session = self.mox.CreateMockAnything()
session.call_plugin_serialized('glance', 'upload_vhd', **params)
self.mox.ReplayAll()
self.store.upload_image(ctx, session, instance, vdi_uuids, image_id)
self.mox.VerifyAll()

View File

View File

@@ -0,0 +1,54 @@
# Copyright 2013 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.
from nova.image import glance
from nova.openstack.common import cfg
import nova.openstack.common.log as logging
from nova.virt.xenapi import vm_utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class GlanceStore(object):
def upload_image(self, context, session, instance, vdi_uuids, image_id):
"""Requests that the Glance plugin bundle the specified VDIs and
push them into Glance using the specified human-friendly name.
"""
# NOTE(sirp): Currently we only support uploading images as VHD, there
# is no RAW equivalent (yet)
LOG.debug(_("Asking xapi to upload to glance %(vdi_uuids)s as"
" ID %(image_id)s"), locals(), instance=instance)
glance_api_servers = glance.get_api_servers()
glance_host, glance_port, glance_use_ssl = glance_api_servers.next()
properties = {
'auto_disk_config': instance['auto_disk_config'],
'os_type': instance['os_type'] or CONF.default_os_type,
}
params = {'vdi_uuids': vdi_uuids,
'image_id': image_id,
'glance_host': glance_host,
'glance_port': glance_port,
'glance_use_ssl': glance_use_ssl,
'sr_path': vm_utils.get_sr_path(session),
'auth_token': getattr(context, 'auth_token', None),
'properties': properties}
session.call_plugin_serialized('glance', 'upload_vhd', **params)

View File

@@ -722,35 +722,6 @@ def _find_cached_image(session, image_id, sr_ref):
return cached_images.get(image_id) return cached_images.get(image_id)
def upload_image(context, session, instance, vdi_uuids, image_id):
"""Requests that the Glance plugin bundle the specified VDIs and
push them into Glance using the specified human-friendly name.
"""
# NOTE(sirp): Currently we only support uploading images as VHD, there
# is no RAW equivalent (yet)
LOG.debug(_("Asking xapi to upload %(vdi_uuids)s as"
" ID %(image_id)s"), locals(), instance=instance)
glance_api_servers = glance.get_api_servers()
glance_host, glance_port, glance_use_ssl = glance_api_servers.next()
properties = {
'auto_disk_config': instance['auto_disk_config'],
'os_type': instance['os_type'] or CONF.default_os_type,
}
params = {'vdi_uuids': vdi_uuids,
'image_id': image_id,
'glance_host': glance_host,
'glance_port': glance_port,
'glance_use_ssl': glance_use_ssl,
'sr_path': get_sr_path(session),
'auth_token': getattr(context, 'auth_token', None),
'properties': properties}
session.call_plugin_serialized('glance', 'upload_vhd', **params)
def resize_disk(session, instance, vdi_ref, instance_type): def resize_disk(session, instance, vdi_ref, instance_type):
# Copy VDI over to something we can resize # Copy VDI over to something we can resize
# NOTE(jerdfelt): Would be nice to just set vdi_ref to read/write # NOTE(jerdfelt): Would be nice to just set vdi_ref to read/write

View File

@@ -59,7 +59,10 @@ xenapi_vmops_opts = [
'to go to running state'), 'to go to running state'),
cfg.StrOpt('xenapi_vif_driver', cfg.StrOpt('xenapi_vif_driver',
default='nova.virt.xenapi.vif.XenAPIBridgeDriver', default='nova.virt.xenapi.vif.XenAPIBridgeDriver',
help='The XenAPI VIF driver using XenServer Network APIs.') help='The XenAPI VIF driver using XenServer Network APIs.'),
cfg.StrOpt('xenapi_image_upload_handler',
default='nova.virt.xenapi.imageupload.glance.GlanceStore',
help='Object Store Driver used to handle image uploads.'),
] ]
CONF = cfg.CONF CONF = cfg.CONF
@@ -161,6 +164,11 @@ class VMOps(object):
self.vif_driver = vif_impl(xenapi_session=self._session) self.vif_driver = vif_impl(xenapi_session=self._session)
self.default_root_dev = '/dev/sda' self.default_root_dev = '/dev/sda'
msg = _("Importing image upload handler: %s")
LOG.debug(msg % CONF.xenapi_image_upload_handler)
self.image_upload_handler = importutils.import_object(
CONF.xenapi_image_upload_handler)
@property @property
def agent_enabled(self): def agent_enabled(self):
return not CONF.xenapi_disable_agent return not CONF.xenapi_disable_agent
@@ -661,9 +669,11 @@ class VMOps(object):
coalesce together, so, we must wait for this coalescing to occur to coalesce together, so, we must wait for this coalescing to occur to
get a stable representation of the data on disk. get a stable representation of the data on disk.
3. Push-to-glance: Once coalesced, we call a plugin on the XenServer 3. Push-to-data-store: Once coalesced, we call a plugin on the
that will bundle the VHDs together and then push the bundle into XenServer that will bundle the VHDs together and then push the
Glance. bundle. Depending on the configured value of
'xenapi_image_upload_handler', image data may be pushed to
Glance or the specified data store.
""" """
vm_ref = self._get_vm_opaque_ref(instance) vm_ref = self._get_vm_opaque_ref(instance)
@@ -674,8 +684,11 @@ class VMOps(object):
update_task_state) as vdi_uuids: update_task_state) as vdi_uuids:
update_task_state(task_state=task_states.IMAGE_UPLOADING, update_task_state(task_state=task_states.IMAGE_UPLOADING,
expected_state=task_states.IMAGE_PENDING_UPLOAD) expected_state=task_states.IMAGE_PENDING_UPLOAD)
vm_utils.upload_image( self.image_upload_handler.upload_image(context,
context, self._session, instance, vdi_uuids, image_id) self._session,
instance,
vdi_uuids,
image_id)
LOG.debug(_("Finished snapshot and upload for VM"), LOG.debug(_("Finished snapshot and upload for VM"),
instance=instance) instance=instance)