diff options
author | Rick Harris <rick.harris@rackspace.com> | 2010-12-30 21:09:27 +0000 |
---|---|---|
committer | Tarmac <> | 2010-12-30 21:09:27 +0000 |
commit | ac6fd2290febed0238b67ac0cd83b220cdbd3efc (patch) | |
tree | 0b02680d340843d5d646ec7d800a672d51ed26b4 | |
parent | 0880026253d0de67afd7c80415b332f72764457d (diff) | |
parent | 601b19291a7cf1bcda7bcd4ebf27e4eefe3e28fd (diff) | |
download | nova-ac6fd2290febed0238b67ac0cd83b220cdbd3efc.tar.gz nova-ac6fd2290febed0238b67ac0cd83b220cdbd3efc.tar.xz nova-ac6fd2290febed0238b67ac0cd83b220cdbd3efc.zip |
This patch is beginning of XenServer snapshots in nova. It adds:
1. A snapshot() method to the compute API
2. Snapshot to the compute drivers(), stubs for libvirt and fake, and an implementation for xenapi_conn.
3. A `glance` XenServer plugin (modeled after the current `objectstore` plugin).
To test functionally you will need to be running both the Glance API and the Glance Registry.
In order to use the cloudservers tool-chain (cloudservers command-line utility, openstack-api, etc), you will need create a project called 'openstack' or override FLAGS.default_project.
You can verify that snapshots work by using the cloudservers command-line utility to run:
cloudservers image-create <instance-id> <my snapshot name>
The utility itself will generate an exception since we're not returning the appropriate metadata yet (that will be a future commit); however, you can confirm that the snapshot worked by checking the compute-service logs, and verifying the snapshot resides in the glance fs-store directory.
-rw-r--r-- | .mailmap | 1 | ||||
-rw-r--r-- | Authors | 1 | ||||
-rw-r--r-- | nova/api/openstack/auth.py | 3 | ||||
-rw-r--r-- | nova/api/openstack/images.py | 10 | ||||
-rw-r--r-- | nova/compute/api.py | 9 | ||||
-rw-r--r-- | nova/compute/manager.py | 21 | ||||
-rw-r--r-- | nova/flags.py | 3 | ||||
-rw-r--r-- | nova/image/glance.py | 1 | ||||
-rw-r--r-- | nova/tests/api/openstack/fakes.py | 9 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_images.py | 11 | ||||
-rw-r--r-- | nova/tests/test_compute.py | 8 | ||||
-rw-r--r-- | nova/tests/test_xenapi.py | 106 | ||||
-rw-r--r-- | nova/tests/xenapi/stubs.py | 66 | ||||
-rw-r--r-- | nova/virt/fake.py | 14 | ||||
-rw-r--r-- | nova/virt/libvirt_conn.py | 7 | ||||
-rw-r--r-- | nova/virt/xenapi/fake.py | 64 | ||||
-rw-r--r-- | nova/virt/xenapi/vm_utils.py | 140 | ||||
-rw-r--r-- | nova/virt/xenapi/vmops.py | 73 | ||||
-rw-r--r-- | nova/virt/xenapi_conn.py | 10 | ||||
-rw-r--r-- | plugins/xenserver/xenapi/etc/xapi.d/plugins/glance | 132 |
20 files changed, 626 insertions, 63 deletions
@@ -27,5 +27,6 @@ <vishvananda@gmail.com> <root@ubuntu> <sleepsonthefloor@gmail.com> <root@tonbuntu> <rlane@wikimedia.org> <laner@controller> +<rconradharris@gmail.com> <rick.harris@rackspace.com> <corywright@gmail.com> <cory.wright@rackspace.com> <ant@openstack.org> <amesserl@rackspace.com> @@ -27,6 +27,7 @@ Michael Gundlach <michael.gundlach@rackspace.com> Monty Taylor <mordred@inaugust.com> Paul Voccio <paul@openstack.org> Rick Clark <rick@openstack.org> +Rick Harris <rconradharris@gmail.com> Ryan Lane <rlane@wikimedia.org> Ryan Lucio <rlucio@internap.com> Salvatore Orlando <salvatore.orlando@eu.citrix.com> diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index e24e58fd3..1dfdd5318 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -55,7 +55,8 @@ class AuthMiddleware(wsgi.Middleware): if not user: return faults.Fault(webob.exc.HTTPUnauthorized()) - req.environ['nova.context'] = context.RequestContext(user, user) + project = self.auth.get_project(FLAGS.default_project) + req.environ['nova.context'] = context.RequestContext(user, project) return self.application def has_authentication(self, req): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index ba35fbc78..867ee5a7e 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -25,7 +25,7 @@ import nova.image.service from nova.api.openstack import common from nova.api.openstack import faults - +from nova.compute import api as compute_api FLAGS = flags.FLAGS @@ -127,9 +127,11 @@ class Controller(wsgi.Controller): raise faults.Fault(exc.HTTPNotFound()) def create(self, req): - # Only public images are supported for now, so a request to - # make a backup of a server cannot be supproted. - raise faults.Fault(exc.HTTPNotFound()) + context = req.environ['nova.context'] + env = self._deserialize(req.body, req) + instance_id = env["image"]["serverId"] + name = env["image"]["name"] + return compute_api.ComputeAPI().snapshot(context, instance_id, name) def update(self, req, id): # Users may not modify public images, and that's all that diff --git a/nova/compute/api.py b/nova/compute/api.py index a47703461..8505b34ed 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -257,6 +257,15 @@ class ComputeAPI(base.Base): def get_instance(self, context, instance_id): return self.db.instance_get_by_internal_id(context, instance_id) + def snapshot(self, context, instance_id, name): + """Snapshot the given instance.""" + instance = self.db.instance_get_by_internal_id(context, instance_id) + host = instance['host'] + rpc.cast(context, + self.db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "snapshot_instance", + "args": {"instance_id": instance['id'], "name": name}}) + def reboot(self, context, instance_id): """Reboot the given instance.""" instance = self.db.instance_get_by_internal_id(context, instance_id) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 70b175e7c..e00c78764 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -225,6 +225,27 @@ class ComputeManager(manager.Manager): self._update_state(context, instance_id) @exception.wrap_exception + def snapshot_instance(self, context, instance_id, name): + """Snapshot an instance on this server.""" + context = context.elevated() + instance_ref = self.db.instance_get(context, instance_id) + + #NOTE(sirp): update_state currently only refreshes the state field + # if we add is_snapshotting, we will need this refreshed too, + # potentially? + self._update_state(context, instance_id) + + logging.debug(_('instance %s: snapshotting'), instance_ref['name']) + if instance_ref['state'] != power_state.RUNNING: + logging.warn(_('trying to snapshot a non-running ' + 'instance: %s (state: %s excepted: %s)'), + instance_ref['internal_id'], + instance_ref['state'], + power_state.RUNNING) + + self.driver.snapshot(instance_ref, name) + + @exception.wrap_exception def rescue_instance(self, context, instance_id): """Rescue an instance on this server.""" context = context.elevated() diff --git a/nova/flags.py b/nova/flags.py index 76a98d35a..d773a7e4c 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -212,6 +212,8 @@ DEFINE_list('region_list', DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_string('aws_access_key_id', 'admin', 'AWS Access ID') DEFINE_string('aws_secret_access_key', 'admin', 'AWS Access Key') +DEFINE_integer('glance_port', 9292, 'glance port') +DEFINE_string('glance_host', utils.get_my_ip(), 'glance host') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', utils.get_my_ip(), 's3 host (for infrastructure)') DEFINE_string('s3_dmz', utils.get_my_ip(), 's3 dmz ip (for instances)') @@ -239,6 +241,7 @@ DEFINE_string('cc_dmz', utils.get_my_ip(), 'internal ip of api server') DEFINE_integer('cc_port', 8773, 'cloud controller port') DEFINE_string('ec2_suffix', '/services/Cloud', 'suffix for ec2') +DEFINE_string('default_project', 'openstack', 'default project for openstack') DEFINE_string('default_image', 'ami-11111', 'default image to use, testing only') DEFINE_string('default_instance_type', 'm1.small', diff --git a/nova/image/glance.py b/nova/image/glance.py index cb3936df1..cc3192e7c 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -24,6 +24,7 @@ import urlparse import webob.exc +from nova.compute import api as compute_api from nova import utils from nova import flags from nova import exception diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 79663e43a..961431154 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -110,6 +110,12 @@ def stub_out_networking(stubs): stubs.Set(nova.utils, 'get_my_ip', get_my_ip) +def stub_out_compute_api_snapshot(stubs): + def snapshot(self, context, instance_id, name): + return 123 + stubs.Set(nova.compute.api.ComputeAPI, 'snapshot', snapshot) + + def stub_out_glance(stubs, initial_fixtures=[]): class FakeParallaxClient: @@ -213,6 +219,9 @@ class FakeAuthManager(object): return v return None + def get_project(self, pid): + return None + def get_user_from_access_key(self, key): return FakeAuthManager.auth_data.get(key, None) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 1b4031217..0f274bd15 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -50,7 +50,7 @@ class BaseImageServiceTests(object): 'updated': None, 'created': None, 'status': None, - 'serverId': None, + 'instance_id': None, 'progress': None} num_images = len(self.service.index(self.context)) @@ -67,7 +67,7 @@ class BaseImageServiceTests(object): 'updated': None, 'created': None, 'status': None, - 'serverId': None, + 'instance_id': None, 'progress': None} num_images = len(self.service.index(self.context)) @@ -87,7 +87,7 @@ class BaseImageServiceTests(object): 'updated': None, 'created': None, 'status': None, - 'serverId': None, + 'instance_id': None, 'progress': None} id = self.service.create(self.context, fixture) @@ -105,13 +105,13 @@ class BaseImageServiceTests(object): 'updated': None, 'created': None, 'status': None, - 'serverId': None, + 'instance_id': None, 'progress': None}, {'name': 'test image 2', 'updated': None, 'created': None, 'status': None, - 'serverId': None, + 'instance_id': None, 'progress': None}] num_images = len(self.service.index(self.context)) @@ -155,6 +155,7 @@ class GlanceImageServiceTest(unittest.TestCase, def setUp(self): self.stubs = stubout.StubOutForTesting() fakes.stub_out_glance(self.stubs) + fakes.stub_out_compute_api_snapshot(self.stubs) service_class = 'nova.image.glance.GlanceImageService' self.service = utils.import_object(service_class) self.context = context.RequestContext(None, None) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index bcb8a1526..1fb9143f1 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -151,6 +151,14 @@ class ComputeTestCase(test.TestCase): self.compute.reboot_instance(self.context, instance_id) self.compute.terminate_instance(self.context, instance_id) + def test_snapshot(self): + """Ensure instance can be snapshotted""" + instance_id = self._create_instance() + name = "myfakesnapshot" + self.compute.run_instance(self.context, instance_id) + self.compute.snapshot_instance(self.context, instance_id, name) + self.compute.terminate_instance(self.context, instance_id) + def test_console_output(self): """Make sure we can get console output from instance""" instance_id = self._create_instance() diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index ed2e4ffde..f0d84e9aa 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -29,9 +29,9 @@ from nova.auth import manager from nova.compute import instance_types from nova.compute import power_state from nova.virt import xenapi_conn -from nova.virt.xenapi import fake +from nova.virt.xenapi import fake as xenapi_fake from nova.virt.xenapi import volume_utils -from nova.tests.db import fakes +from nova.tests.db import fakes as db_fakes from nova.tests.xenapi import stubs FLAGS = flags.FLAGS @@ -47,9 +47,9 @@ class XenAPIVolumeTestCase(test.TestCase): FLAGS.target_host = '127.0.0.1' FLAGS.xenapi_connection_url = 'test_url' FLAGS.xenapi_connection_password = 'test_pass' - fakes.stub_out_db_instance_api(self.stubs) + db_fakes.stub_out_db_instance_api(self.stubs) stubs.stub_out_get_target(self.stubs) - fake.reset() + xenapi_fake.reset() self.values = {'name': 1, 'id': 1, 'project_id': 'fake', 'user_id': 'fake', @@ -83,7 +83,7 @@ class XenAPIVolumeTestCase(test.TestCase): label = 'SR-%s' % vol['ec2_id'] description = 'Test-SR' sr_ref = helper.create_iscsi_storage(session, info, label, description) - srs = fake.get_all('SR') + srs = xenapi_fake.get_all('SR') self.assertEqual(sr_ref, srs[0]) db.volume_destroy(context.get_admin_context(), vol['id']) @@ -107,17 +107,17 @@ class XenAPIVolumeTestCase(test.TestCase): conn = xenapi_conn.get_connection(False) volume = self._create_volume() instance = db.instance_create(self.values) - fake.create_vm(instance.name, 'Running') + xenapi_fake.create_vm(instance.name, 'Running') result = conn.attach_volume(instance.name, volume['ec2_id'], '/dev/sdc') def check(): # check that the VM has a VBD attached to it # Get XenAPI reference for the VM - vms = fake.get_all('VM') + vms = xenapi_fake.get_all('VM') # Get XenAPI record for VBD - vbds = fake.get_all('VBD') - vbd = fake.get_record('VBD', vbds[0]) + vbds = xenapi_fake.get_all('VBD') + vbd = xenapi_fake.get_record('VBD', vbds[0]) vm_ref = vbd['VM'] self.assertEqual(vm_ref, vms[0]) @@ -130,7 +130,7 @@ class XenAPIVolumeTestCase(test.TestCase): conn = xenapi_conn.get_connection(False) volume = self._create_volume() instance = db.instance_create(self.values) - fake.create_vm(instance.name, 'Running') + xenapi_fake.create_vm(instance.name, 'Running') self.assertRaises(Exception, conn.attach_volume, instance.name, @@ -156,41 +156,66 @@ class XenAPIVMTestCase(test.TestCase): self.stubs = stubout.StubOutForTesting() FLAGS.xenapi_connection_url = 'test_url' FLAGS.xenapi_connection_password = 'test_pass' - fake.reset() - fakes.stub_out_db_instance_api(self.stubs) - fake.create_network('fake', FLAGS.flat_network_bridge) + xenapi_fake.reset() + db_fakes.stub_out_db_instance_api(self.stubs) + xenapi_fake.create_network('fake', FLAGS.flat_network_bridge) + stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests) + self.conn = xenapi_conn.get_connection(False) def test_list_instances_0(self): - stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests) - conn = xenapi_conn.get_connection(False) - instances = conn.list_instances() + instances = self.conn.list_instances() self.assertEquals(instances, []) + def test_instance_snapshot(self): + stubs.stubout_instance_snapshot(self.stubs) + instance = self._create_instance() + + name = "MySnapshot" + template_vm_ref = self.conn.snapshot(instance, name) + + def ensure_vm_was_torn_down(): + vm_labels = [] + for vm_ref in xenapi_fake.get_all('VM'): + vm_rec = xenapi_fake.get_record('VM', vm_ref) + if not vm_rec["is_control_domain"]: + vm_labels.append(vm_rec["name_label"]) + + self.assertEquals(vm_labels, [1]) + + def ensure_vbd_was_torn_down(): + vbd_labels = [] + for vbd_ref in xenapi_fake.get_all('VBD'): + vbd_rec = xenapi_fake.get_record('VBD', vbd_ref) + vbd_labels.append(vbd_rec["vm_name_label"]) + + self.assertEquals(vbd_labels, [1]) + + def ensure_vdi_was_torn_down(): + for vdi_ref in xenapi_fake.get_all('VDI'): + vdi_rec = xenapi_fake.get_record('VDI', vdi_ref) + name_label = vdi_rec["name_label"] + self.assert_(not name_label.endswith('snapshot')) + + def check(): + ensure_vm_was_torn_down() + ensure_vbd_was_torn_down() + ensure_vdi_was_torn_down() + + check() + def test_spawn(self): - stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests) - values = {'name': 1, 'id': 1, - 'project_id': self.project.id, - 'user_id': self.user.id, - 'image_id': 1, - 'kernel_id': 2, - 'ramdisk_id': 3, - 'instance_type': 'm1.large', - 'mac_address': 'aa:bb:cc:dd:ee:ff', - } - conn = xenapi_conn.get_connection(False) - instance = db.instance_create(values) - conn.spawn(instance) + instance = self._create_instance() def check(): - instances = conn.list_instances() + instances = self.conn.list_instances() self.assertEquals(instances, [1]) # Get Nova record for VM - vm_info = conn.get_info(1) + vm_info = self.conn.get_info(1) # Get XenAPI record for VM - vms = fake.get_all('VM') - vm = fake.get_record('VM', vms[0]) + vms = xenapi_fake.get_all('VM') + vm = xenapi_fake.get_record('VM', vms[0]) # Check that m1.large above turned into the right thing. instance_type = instance_types.INSTANCE_TYPES['m1.large'] @@ -218,3 +243,18 @@ class XenAPIVMTestCase(test.TestCase): self.manager.delete_project(self.project) self.manager.delete_user(self.user) self.stubs.UnsetAll() + + def _create_instance(self): + """Creates and spawns a test instance""" + values = {'name': 1, 'id': 1, + 'project_id': self.project.id, + 'user_id': self.user.id, + 'image_id': 1, + 'kernel_id': 2, + 'ramdisk_id': 3, + 'instance_type': 'm1.large', + 'mac_address': 'aa:bb:cc:dd:ee:ff', + } + instance = db.instance_create(values) + self.conn.spawn(instance) + return instance diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index a7e592fee..55f751f11 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -19,6 +19,54 @@ from nova.virt import xenapi_conn from nova.virt.xenapi import fake from nova.virt.xenapi import volume_utils +from nova.virt.xenapi import vm_utils + + +def stubout_instance_snapshot(stubs): + @classmethod + def fake_fetch_image(cls, session, instance_id, image, user, project, + type): + # Stubout wait_for_task + def fake_wait_for_task(self, id, task): + class FakeEvent: + + def send(self, value): + self.rv = value + + def wait(self): + return self.rv + + done = FakeEvent() + self._poll_task(id, task, done) + rv = done.wait() + return rv + + stubs.Set(xenapi_conn.XenAPISession, 'wait_for_task', + fake_wait_for_task) + + from nova.virt.xenapi.fake import create_vdi + name_label = "instance-%s" % instance_id + #TODO: create fake SR record + sr_ref = "fakesr" + vdi_ref = create_vdi(name_label=name_label, read_only=False, + sr_ref=sr_ref, sharable=False) + vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref) + vdi_uuid = vdi_rec['uuid'] + return vdi_uuid + + stubs.Set(vm_utils.VMHelper, 'fetch_image', fake_fetch_image) + + def fake_parse_xmlrpc_value(val): + return val + + stubs.Set(xenapi_conn, '_parse_xmlrpc_value', fake_parse_xmlrpc_value) + + def fake_wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref, + original_parent_uuid): + #TODO(sirp): Should we actually fake out the data here + return "fakeparent" + + stubs.Set(vm_utils, 'wait_for_vhd_coalesce', fake_wait_for_vhd_coalesce) def stubout_session(stubs, cls): @@ -63,6 +111,24 @@ class FakeSessionForVMTests(fake.SessionBase): vm['is_a_template'] = False vm['is_control_domain'] = False + def VM_snapshot(self, session_ref, vm_ref, label): + status = "Running" + template_vm_ref = fake.create_vm(label, status, is_a_template=True, + is_control_domain=False) + + sr_ref = "fakesr" + template_vdi_ref = fake.create_vdi(label, read_only=True, + sr_ref=sr_ref, sharable=False) + + template_vbd_ref = fake.create_vbd(template_vm_ref, template_vdi_ref) + return template_vm_ref + + def VDI_destroy(self, session_ref, vdi_ref): + fake.destroy_vdi(vdi_ref) + + def VM_destroy(self, session_ref, vm_ref): + fake.destroy_vm(vm_ref) + class FakeSessionForVolumeTests(fake.SessionBase): """ Stubs out a XenAPISession for Volume tests """ diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 706888b0d..49100b1df 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -112,6 +112,20 @@ class FakeConnection(object): self.instances[instance.name] = fake_instance fake_instance._state = power_state.RUNNING + def snapshot(self, instance, name): + """ + Snapshots the specified instance. + + The given parameter is an instance of nova.compute.service.Instance, + and so the instance is being specified as instance.name. + + The second parameter is the name of the snapshot. + + The work will be done asynchronously. This function returns a + Deferred that allows the caller to detect when it is complete. + """ + pass + def reboot(self, instance): """ Reboot the specified instance. diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 39215c4e1..3a9e58b50 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -260,6 +260,13 @@ class LibvirtConnection(object): virt_dom.detachDevice(xml) @exception.wrap_exception + def snapshot(self, instance, name): + """ Create snapshot from a running VM instance """ + raise NotImplementedError( + _("Instance snapshotting is not supported for libvirt" + "at this time")) + + @exception.wrap_exception def reboot(self, instance): self.destroy(instance, False) xml = self.to_xml(instance) diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index 1eaf31c25..aa4026f97 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -55,6 +55,8 @@ import datetime import logging import uuid +from pprint import pformat + from nova import exception @@ -64,6 +66,10 @@ _CLASSES = ['host', 'network', 'session', 'SR', 'VBD',\ _db_content = {} +def log_db_contents(msg=None): + logging.debug(_("%s: _db_content => %s"), msg or "", pformat(_db_content)) + + def reset(): for c in _CLASSES: _db_content[c] = {} @@ -93,6 +99,24 @@ def create_vm(name_label, status, }) +def destroy_vm(vm_ref): + vm_rec = _db_content['VM'][vm_ref] + + vbd_refs = vm_rec['VBDs'] + for vbd_ref in vbd_refs: + destroy_vbd(vbd_ref) + + del _db_content['VM'][vm_ref] + + +def destroy_vbd(vbd_ref): + del _db_content['VBD'][vbd_ref] + + +def destroy_vdi(vdi_ref): + del _db_content['VDI'][vdi_ref] + + def create_vdi(name_label, read_only, sr_ref, sharable): return _create_object('VDI', { 'name_label': name_label, @@ -109,6 +133,23 @@ def create_vdi(name_label, read_only, sr_ref, sharable): }) +def create_vbd(vm_ref, vdi_ref): + vbd_rec = {'VM': vm_ref, 'VDI': vdi_ref} + vbd_ref = _create_object('VBD', vbd_rec) + after_VBD_create(vbd_ref, vbd_rec) + return vbd_ref + + +def after_VBD_create(vbd_ref, vbd_rec): + """Create backref from VM to VBD when VBD is created""" + vm_ref = vbd_rec['VM'] + vm_rec = _db_content['VM'][vm_ref] + vm_rec['VBDs'] = [vbd_ref] + + vm_name_label = _db_content['VM'][vm_ref]['name_label'] + vbd_rec['vm_name_label'] = vm_name_label + + def create_pbd(config, sr_ref, attached): return _create_object('PBD', { 'device-config': config, @@ -277,11 +318,12 @@ class SessionBase(object): self._check_arg_count(params, 2) return get_record(cls, params[1]) - if (func == 'get_by_name_label' or - func == 'get_by_uuid'): + if func in ('get_by_name_label', 'get_by_uuid'): self._check_arg_count(params, 2) + return_singleton = (func == 'get_by_uuid') return self._get_by_field( - _db_content[cls], func[len('get_by_'):], params[1]) + _db_content[cls], func[len('get_by_'):], params[1], + return_singleton=return_singleton) if len(params) == 2: field = func[len('get_'):] @@ -324,6 +366,13 @@ class SessionBase(object): (cls, _) = name.split('.') ref = is_sr_create and \ _create_sr(cls, params) or _create_object(cls, params[1]) + + # Call hook to provide any fixups needed (ex. creating backrefs) + try: + globals()["after_%s_create" % cls](ref, params[1]) + except KeyError: + pass + obj = get_record(cls, ref) # Add RO fields @@ -359,11 +408,18 @@ class SessionBase(object): raise Failure(['MESSAGE_PARAMETER_COUNT_MISMATCH', expected, actual]) - def _get_by_field(self, recs, k, v): + def _get_by_field(self, recs, k, v, return_singleton): result = [] for ref, rec in recs.iteritems(): if rec.get(k) == v: result.append(ref) + + if return_singleton: + try: + return result[0] + except IndexError: + return None + return result diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 47fb6db53..218c147ce 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -20,11 +20,14 @@ their attributes like VDIs, VIFs, as well as their lookup functions. """ import logging +import pickle import urllib from xml.dom import minidom +from eventlet import event from nova import exception from nova import flags +from nova import utils from nova.auth.manager import AuthManager from nova.compute import instance_types from nova.compute import power_state @@ -204,7 +207,54 @@ class VMHelper(HelperBase): return vif_ref @classmethod - def fetch_image(cls, session, image, user, project, type): + def create_snapshot(cls, session, instance_id, vm_ref, label): + """ Creates Snapshot (Template) VM, Snapshot VBD, Snapshot VDI, + Snapshot VHD + """ + #TODO(sirp): Add quiesce and VSS locking support when Windows support + # is added + logging.debug(_("Snapshotting VM %s with label '%s'..."), + vm_ref, label) + + vm_vdi_ref, vm_vdi_rec = get_vdi_for_vm_safely(session, vm_ref) + vm_vdi_uuid = vm_vdi_rec["uuid"] + sr_ref = vm_vdi_rec["SR"] + + original_parent_uuid = get_vhd_parent_uuid(session, vm_vdi_ref) + + task = session.call_xenapi('Async.VM.snapshot', vm_ref, label) + template_vm_ref = session.wait_for_task(instance_id, task) + template_vdi_rec = get_vdi_for_vm_safely(session, template_vm_ref)[1] + template_vdi_uuid = template_vdi_rec["uuid"] + + logging.debug(_('Created snapshot %s from VM %s.'), template_vm_ref, + vm_ref) + + parent_uuid = wait_for_vhd_coalesce( + session, instance_id, sr_ref, vm_vdi_ref, original_parent_uuid) + + #TODO(sirp): we need to assert only one parent, not parents two deep + return template_vm_ref, [template_vdi_uuid, parent_uuid] + + @classmethod + def upload_image(cls, session, instance_id, vdi_uuids, image_name): + """ Requests that the Glance plugin bundle the specified VDIs and + push them into Glance using the specified human-friendly name. + """ + logging.debug(_("Asking xapi to upload %s as '%s'"), + vdi_uuids, image_name) + + params = {'vdi_uuids': vdi_uuids, + 'image_name': image_name, + 'glance_host': FLAGS.glance_host, + 'glance_port': FLAGS.glance_port} + + kwargs = {'params': pickle.dumps(params)} + task = session.async_call_plugin('glance', 'put_vdis', kwargs) + session.wait_for_task(instance_id, task) + + @classmethod + def fetch_image(cls, session, instance_id, image, user, project, type): """ type is interpreted as an ImageType instance """ @@ -223,9 +273,7 @@ class VMHelper(HelperBase): if type == ImageType.DISK_RAW: args['raw'] = 'true' task = session.async_call_plugin('objectstore', fn, args) - #FIXME(armando): find a solution to missing instance_id - #with Josh Kearney - uuid = session.wait_for_task(0, task) + uuid = session.wait_for_task(instance_id, task) return uuid @classmethod @@ -325,3 +373,87 @@ def get_rrd(host, uuid): return xml.read() except IOError: return None + + +#TODO(sirp): This code comes from XS5.6 pluginlib.py, we should refactor to +# use that implmenetation +def get_vhd_parent(session, vdi_rec): + """ + Returns the VHD parent of the given VDI record, as a (ref, rec) pair. + Returns None if we're at the root of the tree. + """ + if 'vhd-parent' in vdi_rec['sm_config']: + parent_uuid = vdi_rec['sm_config']['vhd-parent'] + #NOTE(sirp): changed xenapi -> get_xenapi() + parent_ref = session.get_xenapi().VDI.get_by_uuid(parent_uuid) + parent_rec = session.get_xenapi().VDI.get_record(parent_ref) + #NOTE(sirp): changed log -> logging + logging.debug(_("VHD %s has parent %s"), vdi_rec['uuid'], parent_ref) + return parent_ref, parent_rec + else: + return None + + +def get_vhd_parent_uuid(session, vdi_ref): + vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref) + ret = get_vhd_parent(session, vdi_rec) + if ret: + parent_ref, parent_rec = ret + return parent_rec["uuid"] + else: + return None + + +def scan_sr(session, instance_id, sr_ref): + logging.debug(_("Re-scanning SR %s"), sr_ref) + task = session.call_xenapi('Async.SR.scan', sr_ref) + session.wait_for_task(instance_id, task) + + +def wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref, + original_parent_uuid): + """ Spin until the parent VHD is coalesced into its parent VHD + + Before coalesce: + * original_parent_vhd + * parent_vhd + snapshot + + Atter coalesce: + * parent_vhd + snapshot + """ + #TODO(sirp): we need to timeout this req after a while + + def _poll_vhds(): + scan_sr(session, instance_id, sr_ref) + parent_uuid = get_vhd_parent_uuid(session, vdi_ref) + if original_parent_uuid and (parent_uuid != original_parent_uuid): + logging.debug( + _("Parent %s doesn't match original parent %s, " + "waiting for coalesce..."), + parent_uuid, original_parent_uuid) + else: + done.send(parent_uuid) + + done = event.Event() + loop = utils.LoopingCall(_poll_vhds) + loop.start(FLAGS.xenapi_vhd_coalesce_poll_interval, now=True) + parent_uuid = done.wait() + loop.stop() + return parent_uuid + + +def get_vdi_for_vm_safely(session, vm_ref): + vdi_refs = VMHelper.lookup_vm_vdis(session, vm_ref) + if vdi_refs is None: + raise Exception(_("No VDIs found for VM %s") % vm_ref) + else: + num_vdis = len(vdi_refs) + if num_vdis != 1: + raise Exception(_("Unexpected number of VDIs (%s) found for " + "VM %s") % (num_vdis, vm_ref)) + + vdi_ref = vdi_refs[0] + vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref) + return vdi_ref, vdi_rec diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index ba502ffa2..0517209e2 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -70,7 +70,7 @@ class VMOps(object): disk_image_type = ImageType.DISK else: disk_image_type = ImageType.DISK_RAW - vdi_uuid = VMHelper.fetch_image(self._session, + vdi_uuid = VMHelper.fetch_image(self._session, instance.id, instance.image_id, user, project, disk_image_type) vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid) #Have a look at the VDI and see if it has a PV kernel @@ -79,11 +79,11 @@ class VMOps(object): pv_kernel = VMHelper.lookup_image(self._session, vdi_ref) kernel = None if instance.kernel_id: - kernel = VMHelper.fetch_image(self._session, + kernel = VMHelper.fetch_image(self._session, instance.id, instance.kernel_id, user, project, ImageType.KERNEL_RAMDISK) ramdisk = None if instance.ramdisk_id: - ramdisk = VMHelper.fetch_image(self._session, + ramdisk = VMHelper.fetch_image(self._session, instance.id, instance.ramdisk_id, user, project, ImageType.KERNEL_RAMDISK) vm_ref = VMHelper.create_vm(self._session, instance, kernel, ramdisk, pv_kernel) @@ -120,6 +120,52 @@ class VMOps(object): timer.f = _wait_for_boot return timer.start(interval=0.5, now=True) + def snapshot(self, instance, name): + """ Create snapshot from a running VM instance + + :param instance: instance to be snapshotted + :param name: name/label to be given to the snapshot + + Steps involved in a XenServer snapshot: + + 1. XAPI-Snapshot: Snapshotting the instance using XenAPI. This + creates: Snapshot (Template) VM, Snapshot VBD, Snapshot VDI, + Snapshot VHD + + 2. Wait-for-coalesce: The Snapshot VDI and Instance VDI both point to + a 'base-copy' VDI. The base_copy is immutable and may be chained + with other base_copies. If chained, the base_copies + coalesce together, so, we must wait for this coalescing to occur to + get a stable representation of the data on disk. + + 3. Push-to-glance: Once coalesced, we call a plugin on the XenServer + that will bundle the VHDs together and then push the bundle into + Glance. + """ + + #TODO(sirp): Add quiesce and VSS locking support when Windows support + # is added + + logging.debug(_("Starting snapshot for VM %s"), instance) + vm_ref = VMHelper.lookup(self._session, instance.name) + + label = "%s-snapshot" % instance.name + try: + template_vm_ref, template_vdi_uuids = VMHelper.create_snapshot( + self._session, instance.id, vm_ref, label) + except self.XenAPI.Failure, exc: + logging.error(_("Unable to Snapshot %s: %s"), vm_ref, exc) + return + + try: + # call plugin to ship snapshot off to glance + VMHelper.upload_image( + self._session, instance.id, template_vdi_uuids, name) + finally: + self._destroy(instance, template_vm_ref, shutdown=False) + + logging.debug(_("Finished snapshot and upload for VM %s"), instance) + def reboot(self, instance): """Reboot VM instance""" instance_name = instance.name @@ -133,31 +179,36 @@ class VMOps(object): def destroy(self, instance): """Destroy VM instance""" vm = VMHelper.lookup(self._session, instance.name) + return self._destroy(instance, vm, shutdown=True) + + def _destroy(self, instance, vm, shutdown=True): + """ Destroy VM instance """ if vm is None: # Don't complain, just return. This lets us clean up instances # that have already disappeared from the underlying platform. return # Get the VDIs related to the VM vdis = VMHelper.lookup_vm_vdis(self._session, vm) - try: - task = self._session.call_xenapi('Async.VM.hard_shutdown', - vm) - self._session.wait_for_task(instance.id, task) - except XenAPI.Failure, exc: - logging.warn(exc) + if shutdown: + try: + task = self._session.call_xenapi('Async.VM.hard_shutdown', vm) + self._session.wait_for_task(instance.id, task) + except self.XenAPI.Failure, exc: + logging.warn(exc) + # Disk clean-up if vdis: for vdi in vdis: try: task = self._session.call_xenapi('Async.VDI.destroy', vdi) self._session.wait_for_task(instance.id, task) - except XenAPI.Failure, exc: + except self.XenAPI.Failure, exc: logging.warn(exc) # VM Destroy try: task = self._session.call_xenapi('Async.VM.destroy', vm) self._session.wait_for_task(instance.id, task) - except XenAPI.Failure, exc: + except self.XenAPI.Failure, exc: logging.warn(exc) def _wait_with_callback(self, instance_id, task, callback): diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 7f03d6c2b..d12e78c67 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -83,6 +83,10 @@ flags.DEFINE_float('xenapi_task_poll_interval', 'The interval used for polling of remote tasks ' '(Async.VM.start, etc). Used only if ' 'connection_type=xenapi.') +flags.DEFINE_float('xenapi_vhd_coalesce_poll_interval', + 5.0, + 'The interval used for polling of coalescing vhds.' + ' Used only if connection_type=xenapi.') flags.DEFINE_string('target_host', None, 'iSCSI Target Host') @@ -131,6 +135,10 @@ class XenAPIConnection(object): """Create VM instance""" self._vmops.spawn(instance) + def snapshot(self, instance, name): + """ Create snapshot from a running VM instance """ + self._vmops.snapshot(instance, name) + def reboot(self, instance): """Reboot VM instance""" self._vmops.reboot(instance) @@ -233,7 +241,7 @@ class XenAPISession(object): name = self._session.xenapi.task.get_name_label(task) status = self._session.xenapi.task.get_status(task) action = dict( - id=int(id), + instance_id=int(id), action=name, error=None) if status == "pending": diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance new file mode 100644 index 000000000..5e648b970 --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance @@ -0,0 +1,132 @@ +#!/usr/bin/env python + +# Copyright (c) 2010 Citrix Systems, Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +# +# XenAPI plugin for putting images into glance +# + +import base64 +import errno +import hmac +import httplib +import os +import os.path +import pickle +import sha +import subprocess +import time +import urlparse + +import XenAPIPlugin + +#FIXME(sirp): should this use pluginlib from 5.6? +from pluginlib_nova import * +configure_logging('glance') + +CHUNK_SIZE = 8192 +FILE_SR_PATH = '/var/run/sr-mount' + +def put_vdis(session, args): + params = pickle.loads(exists(args, 'params')) + vdi_uuids = params["vdi_uuids"] + image_name = params["image_name"] + glance_host = params["glance_host"] + glance_port = params["glance_port"] + + sr_path = get_sr_path(session) + #FIXME(sirp): writing to a temp file until Glance supports chunked-PUTs + tmp_file = "%s.tar.gz" % os.path.join('/tmp', image_name) + tar_cmd = ['tar', '-zcf', tmp_file, '--directory=%s' % sr_path] + paths = [ "%s.vhd" % vdi_uuid for vdi_uuid in vdi_uuids ] + tar_cmd.extend(paths) + logging.debug("Bundling image with cmd: %s", tar_cmd) + subprocess.call(tar_cmd) + logging.debug("Writing to test file %s", tmp_file) + put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port) + return "" # FIXME(sirp): return anything useful here? + + +def put_bundle_in_glance(tmp_file, image_name, glance_host, glance_port): + size = os.path.getsize(tmp_file) + basename = os.path.basename(tmp_file) + + bundle = open(tmp_file, 'r') + try: + headers = { + 'x-image-meta-store': 'file', + 'x-image-meta-is_public': 'True', + 'x-image-meta-type': 'raw', + 'x-image-meta-name': image_name, + 'x-image-meta-size': size, + 'content-length': size, + 'content-type': 'application/octet-stream', + } + conn = httplib.HTTPConnection(glance_host, glance_port) + #NOTE(sirp): httplib under python2.4 won't accept a file-like object + # to request + conn.putrequest('POST', '/images') + + for header, value in headers.iteritems(): + conn.putheader(header, value) + conn.endheaders() + + chunk = bundle.read(CHUNK_SIZE) + while chunk: + conn.send(chunk) + chunk = bundle.read(CHUNK_SIZE) + + + res = conn.getresponse() + #FIXME(sirp): should this be 201 Created? + if res.status != httplib.OK: + raise Exception("Unexpected response from Glance %i" % res.status) + finally: + bundle.close() + +def get_sr_path(session): + sr_ref = find_sr(session) + + if sr_ref is None: + raise Exception('Cannot find SR to read VDI from') + + sr_rec = session.xenapi.SR.get_record(sr_ref) + sr_uuid = sr_rec["uuid"] + sr_path = os.path.join(FILE_SR_PATH, sr_uuid) + return sr_path + + +#TODO(sirp): both objectstore and glance need this, should this be refactored +#into common lib +def find_sr(session): + host = get_this_host(session) + srs = session.xenapi.SR.get_all() + for sr in srs: + sr_rec = session.xenapi.SR.get_record(sr) + if not ('i18n-key' in sr_rec['other_config'] and + sr_rec['other_config']['i18n-key'] == 'local-storage'): + continue + for pbd in sr_rec['PBDs']: + pbd_rec = session.xenapi.PBD.get_record(pbd) + if pbd_rec['host'] == host: + return sr + return None + + +if __name__ == '__main__': + XenAPIPlugin.dispatch({'put_vdis': put_vdis}) |