From 8f51b120b430c7c21399256f37e1d8f75d030484 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 13 May 2013 12:36:56 -0700 Subject: [PATCH] Add support for volume swap Adds support for transparently swapping an attached volume with another volume. Note that this overwrites all data on the new volume with data from the old volume. Implements blueprint volume-swap Change-Id: Iaace71f46acd33cf1531d953d569c0b6d0bbe680 --- .../all_extensions/extensions-get-resp.json | 74 ++++++++------- .../all_extensions/extensions-get-resp.xml | 33 ++++--- .../server-post-req.json | 16 ++++ .../server-post-req.xml | 19 ++++ .../server-post-resp.json | 16 ++++ .../server-post-resp.xml | 6 ++ .../update-volume-req.json | 6 ++ .../update-volume-req.xml | 2 + etc/nova/policy.json | 1 + .../contrib/volume_attachment_update.py | 26 ++++++ nova/api/openstack/compute/contrib/volumes.py | 54 ++++++++++- nova/compute/api.py | 30 ++++++ nova/compute/manager.py | 66 ++++++++++++- nova/compute/rpcapi.py | 8 ++ .../openstack/compute/contrib/test_volumes.py | 77 ++++++++++----- .../api/openstack/compute/test_extensions.py | 1 + nova/tests/compute/test_rpcapi.py | 6 ++ nova/tests/fake_policy.py | 1 + .../extensions-get-resp.json.tpl | 8 ++ .../extensions-get-resp.xml.tpl | 3 + .../server-post-req.json.tpl | 16 ++++ .../server-post-req.xml.tpl | 19 ++++ .../server-post-resp.json.tpl | 16 ++++ .../server-post-resp.xml.tpl | 6 ++ .../update-volume-req.json.tpl | 6 ++ .../update-volume-req.xml.tpl | 2 + nova/tests/integrated/test_api_samples.py | 80 +++++++++++----- nova/tests/virt/test_virt_drivers.py | 11 +++ nova/virt/driver.py | 5 + nova/virt/fake.py | 9 ++ nova/virt/libvirt/driver.py | 93 ++++++++++++++++--- 31 files changed, 603 insertions(+), 113 deletions(-) create mode 100644 doc/api_samples/os-volume-attachment-update/server-post-req.json create mode 100644 doc/api_samples/os-volume-attachment-update/server-post-req.xml create mode 100644 doc/api_samples/os-volume-attachment-update/server-post-resp.json create mode 100644 doc/api_samples/os-volume-attachment-update/server-post-resp.xml create mode 100644 doc/api_samples/os-volume-attachment-update/update-volume-req.json create mode 100644 doc/api_samples/os-volume-attachment-update/update-volume-req.xml create mode 100644 nova/api/openstack/compute/contrib/volume_attachment_update.py create mode 100644 nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.xml.tpl diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index a52c8fd9099e..748f30f8dd6a 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -96,14 +96,6 @@ "namespace": "http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1", "updated": "2011-09-14T00:00:00+00:00" }, - { - "alias": "OS-SRV-USG", - "description": "Adds launched_at and terminated_at on Instances.", - "links": [], - "name": "ServerUsage", - "namespace": "http://docs.openstack.org/compute/ext/server_usage/api/v1.1", - "updated": "2013-04-29T00:00:00+00:00" - }, { "alias": "OS-SCH-HNT", "description": "Pass arbitrary key/value pairs to the scheduler.", @@ -112,6 +104,14 @@ "namespace": "http://docs.openstack.org/compute/ext/scheduler-hints/api/v2", "updated": "2011-07-19T00:00:00+00:00" }, + { + "alias": "OS-SRV-USG", + "description": "Adds launched_at and terminated_at on Servers.", + "links": [], + "name": "ServerUsage", + "namespace": "http://docs.openstack.org/compute/ext/server_usage/api/v1.1", + "updated": "2013-04-29T00:00:00+00:00" + }, { "alias": "os-admin-actions", "description": "Enable admin-only server actions\n\n Actions include: pause, unpause, suspend, resume, migrate,\n resetNetwork, injectNetworkInfo, lock, unlock, createBackup\n ", @@ -160,14 +160,6 @@ "namespace": "http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2", "updated": "2013-01-04T00:00:00+00:00" }, - { - "alias": "os-cells", - "description": "Enables cells-related functionality such as adding neighbor cells,\n listing neighbor cells, and getting the capabilities of the local cell.\n ", - "links": [], - "name": "Cells", - "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", - "updated": "2011-09-21T00:00:00+00:00" - }, { "alias": "os-cell-capacities", "description": "Adding functionality to get cell capacities.", @@ -176,6 +168,14 @@ "namespace": "http://docs.openstack.org/compute/ext/cell_capacities/api/v1.1", "updated": "2013-05-27T00:00:00+00:00" }, + { + "alias": "os-cells", + "description": "Enables cells-related functionality such as adding neighbor cells,\n listing neighbor cells, and getting the capabilities of the local cell.\n ", + "links": [], + "name": "Cells", + "namespace": "http://docs.openstack.org/compute/ext/cells/api/v1.1", + "updated": "2013-05-14T00:00:00+00:00" + }, { "alias": "os-certificates", "description": "Certificates support.", @@ -264,6 +264,22 @@ "namespace": "http://docs.openstack.org/compute/ext/extended_floating_ips/api/v2", "updated": "2013-04-19T00:00:00+00:00" }, + { + "alias": "os-extended-quotas", + "description": "Adds ability for admins to delete quota\n and optionally force the update Quota command.\n ", + "links": [], + "name": "ExtendedQuotas", + "namespace": "http://docs.openstack.org/compute/ext/extended_quotas/api/v1.1", + "updated": "2013-06-09T00:00:00+00:00" + }, + { + "alias": "os-extended-services", + "description": "Extended services support.", + "links": [], + "name": "ExtendedServices", + "namespace": "http://docs.openstack.org/compute/ext/extended_services/api/v2", + "updated": "2013-05-17T00:00:00-00:00" + }, { "alias": "os-fixed-ips", "description": "Fixed IPs support.", @@ -432,14 +448,6 @@ "namespace": "http://docs.openstack.org/compute/ext/quota-classes-sets/api/v1.1", "updated": "2012-03-12T00:00:00+00:00" }, - { - "alias": "os-extended-quotas", - "description": "Adds ability for admins to delete quota and optionally force the update Quota command.", - "links": [], - "name": "ExtendedQuotas", - "namespace": "http://docs.openstack.org/compute/ext/extended_quotas/api/v1.1", - "updated": "2013-06-09T00:00:00+00:00" - }, { "alias": "os-quota-sets", "description": "Quotas management support.", @@ -470,7 +478,7 @@ "links": [], "name": "SecurityGroups", "namespace": "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1", - "updated": "2011-07-21T00:00:00+00:00" + "updated": "2013-05-28T00:00:00+00:00" }, { "alias": "os-server-diagnostics", @@ -504,14 +512,6 @@ "namespace": "http://docs.openstack.org/compute/ext/services/api/v2", "updated": "2012-10-28T00:00:00-00:00" }, - { - "alias": "os-extended-services", - "description": "Extended services support.", - "links": [], - "name": "ExtendedServices", - "namespace": "http://docs.openstack.org/compute/ext/extended_services/api/v2", - "updated": "2013-05-17T00:00:00-00:00" - }, { "alias": "os-shelve", "description": "Instance shelve mode.", @@ -568,6 +568,14 @@ "namespace": "http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1", "updated": "2011-08-17T00:00:00+00:00" }, + { + "alias": "os-volume-attachment-update", + "description": "Support for updating a volume attachment.", + "links": [], + "name": "VolumeAttachmentUpdate", + "namespace": "http://docs.openstack.org/compute/ext/os-volume-attachment-update/api/v2", + "updated": "2013-06-20T00:00:00-00:00" + }, { "alias": "os-volumes", "description": "Volumes support.", diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index 5cac81097d49..22a6c3a162a1 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -36,12 +36,12 @@ Provide additional data for flavors. - - Adds launched_at and terminated_at on Servers. - Pass arbitrary key/value pairs to the scheduler. + + Adds launched_at and terminated_at on Servers. + Enable admin-only server actions @@ -66,14 +66,14 @@ Admin-only bare-metal node administration. - + + Adding functionality to get cell capacities. + + Enables cells-related functionality such as adding neighbor cells, listing neighbor cells, and getting the capabilities of the local cell. - - Adds functionality to get cell capacities. - Certificates support. @@ -115,6 +115,14 @@ Adds optional fixed_address to the add floating IP command. + + Adds ability for admins to delete quota + and optionally force the update Quota command. + + + + Extended services support. + Fixed IPs support. @@ -180,9 +188,6 @@ Quota classes management support. - - Adds ability for admins to delete quota and optionally force the update Quota command. - Quotas management support. @@ -192,7 +197,7 @@ Default rules for security group support. - + Security group support. @@ -207,9 +212,6 @@ Services support. - - Extended services support. - Instance shelve mode. @@ -231,6 +233,9 @@ Virtual interface support. + + Support for updating a volume attachment. + Volumes support. diff --git a/doc/api_samples/os-volume-attachment-update/server-post-req.json b/doc/api_samples/os-volume-attachment-update/server-post-req.json new file mode 100644 index 000000000000..d88eb4122223 --- /dev/null +++ b/doc/api_samples/os-volume-attachment-update/server-post-req.json @@ -0,0 +1,16 @@ +{ + "server" : { + "name" : "new-server-test", + "imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b", + "flavorRef" : "http://openstack.example.com/openstack/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + }, + "personality" : [ + { + "path" : "/etc/banner.txt", + "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/os-volume-attachment-update/server-post-req.xml b/doc/api_samples/os-volume-attachment-update/server-post-req.xml new file mode 100644 index 000000000000..0a3c8bb5303d --- /dev/null +++ b/doc/api_samples/os-volume-attachment-update/server-post-req.xml @@ -0,0 +1,19 @@ + + + + Apache1 + + + + ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp + dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k + IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs + c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g + QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo + ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv + dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy + c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 + b25zLiINCg0KLVJpY2hhcmQgQmFjaA== + + + \ No newline at end of file diff --git a/doc/api_samples/os-volume-attachment-update/server-post-resp.json b/doc/api_samples/os-volume-attachment-update/server-post-resp.json new file mode 100644 index 000000000000..59adf043a528 --- /dev/null +++ b/doc/api_samples/os-volume-attachment-update/server-post-resp.json @@ -0,0 +1,16 @@ +{ + "server": { + "adminPass": "JpSxM7jTxqC2", + "id": "3307a815-3f98-4d01-bf59-9e7c4fb2a6a9", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/servers/3307a815-3f98-4d01-bf59-9e7c4fb2a6a9", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/servers/3307a815-3f98-4d01-bf59-9e7c4fb2a6a9", + "rel": "bookmark" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/os-volume-attachment-update/server-post-resp.xml b/doc/api_samples/os-volume-attachment-update/server-post-resp.xml new file mode 100644 index 000000000000..558e1abbfc3b --- /dev/null +++ b/doc/api_samples/os-volume-attachment-update/server-post-resp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/doc/api_samples/os-volume-attachment-update/update-volume-req.json b/doc/api_samples/os-volume-attachment-update/update-volume-req.json new file mode 100644 index 000000000000..09b42cf99691 --- /dev/null +++ b/doc/api_samples/os-volume-attachment-update/update-volume-req.json @@ -0,0 +1,6 @@ +{ + "volumeAttachment": { + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f805", + "device": "/dev/sdd" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-volume-attachment-update/update-volume-req.xml b/doc/api_samples/os-volume-attachment-update/update-volume-req.xml new file mode 100644 index 000000000000..37d7f84630b1 --- /dev/null +++ b/doc/api_samples/os-volume-attachment-update/update-volume-req.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 2820d8e89a56..ad17f31e3854 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -179,6 +179,7 @@ "compute_extension:volume_attachments:index": "", "compute_extension:volume_attachments:show": "", "compute_extension:volume_attachments:create": "", + "compute_extension:volume_attachments:update": "", "compute_extension:volume_attachments:delete": "", "compute_extension:volumetypes": "", "compute_extension:availability_zone:list": "", diff --git a/nova/api/openstack/compute/contrib/volume_attachment_update.py b/nova/api/openstack/compute/contrib/volume_attachment_update.py new file mode 100644 index 000000000000..bc00e2a94a07 --- /dev/null +++ b/nova/api/openstack/compute/contrib/volume_attachment_update.py @@ -0,0 +1,26 @@ +# Copyright 2013 Nebula, Inc. +# 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.api.openstack import extensions + + +class Volume_attachment_update(extensions.ExtensionDescriptor): + """Support for updating a volume attachment.""" + + name = "VolumeAttachmentUpdate" + alias = "os-volume-attachment-update" + namespace = ("http://docs.openstack.org/compute/ext/" + "os-volume-attachment-update/api/v2") + updated = "2013-06-20T00:00:00-00:00" diff --git a/nova/api/openstack/compute/contrib/volumes.py b/nova/api/openstack/compute/contrib/volumes.py index 55806bc832ce..0ce695a1733a 100644 --- a/nova/api/openstack/compute/contrib/volumes.py +++ b/nova/api/openstack/compute/contrib/volumes.py @@ -326,9 +326,10 @@ class VolumeAttachmentController(wsgi.Controller): """ - def __init__(self): + def __init__(self, ext_mgr=None): self.compute_api = compute.API() self.volume_api = volume.API() + self.ext_mgr = ext_mgr super(VolumeAttachmentController, self).__init__() @wsgi.serializers(xml=VolumeAttachmentsTemplate) @@ -431,8 +432,52 @@ class VolumeAttachmentController(wsgi.Controller): return {'volumeAttachment': attachment} def update(self, req, server_id, id, body): - """Update a volume attachment. We don't currently support this.""" - raise exc.HTTPBadRequest() + if (not self.ext_mgr or + not self.ext_mgr.is_loaded('os-volume-attachment-update')): + raise exc.HTTPBadRequest() + context = req.environ['nova.context'] + authorize(context) + authorize_attach(context, action='update') + + if not self.is_valid_body(body, 'volumeAttachment'): + raise exc.HTTPUnprocessableEntity() + + old_volume_id = id + old_volume = self.volume_api.get(context, old_volume_id) + + new_volume_id = body['volumeAttachment']['volumeId'] + self._validate_volume_id(new_volume_id) + new_volume = self.volume_api.get(context, new_volume_id) + + try: + instance = self.compute_api.get(context, server_id, + want_objects=True) + except exception.NotFound: + raise exc.HTTPNotFound() + + bdms = self.compute_api.get_instance_bdms(context, instance) + found = False + try: + for bdm in bdms: + if bdm['volume_id'] != old_volume_id: + continue + try: + self.compute_api.swap_volume(context, instance, old_volume, + new_volume) + found = True + break + except exception.VolumeUnattached: + # The volume is not attached. Treat it as NotFound + # by falling through. + pass + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'swap_volume') + + if not found: + raise exc.HTTPNotFound() + else: + return webob.Response(status_int=202) def delete(self, req, server_id, id): """Detach a volume from an instance.""" @@ -656,8 +701,9 @@ class Volumes(extensions.ExtensionDescriptor): collection_actions={'detail': 'GET'}) resources.append(res) + attachment_controller = VolumeAttachmentController(self.ext_mgr) res = extensions.ResourceExtension('os-volume_attachments', - VolumeAttachmentController(), + attachment_controller, parent=dict( member_name='server', collection_name='servers')) diff --git a/nova/compute/api.py b/nova/compute/api.py index 862d948cb137..d49699b08192 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -2551,6 +2551,36 @@ class API(base.Base): raise exception.VolumeUnattached(volume_id=volume['id']) self._detach_volume(context, instance, volume) + @wrap_check_policy + @check_instance_lock + @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.PAUSED, + vm_states.SUSPENDED, vm_states.STOPPED, + vm_states.RESIZED, vm_states.SOFT_DELETED], + task_state=None) + def swap_volume(self, context, instance, old_volume, new_volume): + """Swap volume attached to an instance.""" + if old_volume['attach_status'] == 'detached': + msg = _("Old volume must be attached in order to swap.") + raise exception.InvalidVolume(reason=msg) + # The caller likely got the instance from volume['instance_uuid'] + # in the first place, but let's sanity check. + if old_volume['instance_uuid'] != instance['uuid']: + raise exception.VolumeUnattached(volume_id=volume['id']) + if new_volume['attach_status'] == 'attached': + msg = _("New volume must be detached in order to swap.") + raise exception.InvalidVolume(reason=msg) + if int(new_volume['size']) < int(old_volume['size']): + msg = _("New volume must be the same size or larger.") + raise exception.InvalidVolume(reason=msg) + self.volume_api.check_detach(context, old_volume) + self.volume_api.begin_detaching(context, old_volume) + self.volume_api.check_attach(context, new_volume, instance=instance) + self.volume_api.reserve_volume(context, new_volume) + self.compute_rpcapi.swap_volume( + context, instance=instance, + old_volume_id=old_volume['id'], + new_volume_id=new_volume['id']) + @wrap_check_policy def attach_interface(self, context, instance, network_id, port_id, requested_ip): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index df82b06ec772..b1d4a44807b1 100755 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -364,7 +364,7 @@ class ComputeVirtAPI(virtapi.VirtAPI): class ComputeManager(manager.SchedulerDependentManager): """Manages the running instances from creation to destruction.""" - RPC_API_VERSION = '2.33' + RPC_API_VERSION = '2.34' def __init__(self, compute_driver=None, *args, **kwargs): """Load configuration options and connect to the hypervisor.""" @@ -3524,6 +3524,70 @@ class ComputeManager(manager.SchedulerDependentManager): self._notify_about_instance_usage( context, instance, "volume.detach", extra_usage_info=info) + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @reverts_task_state + @wrap_instance_fault + def swap_volume(self, context, old_volume_id, new_volume_id, instance): + """Swap volume for an instance.""" + context = context.elevated() + bdm = self._get_instance_volume_bdm(context, instance, old_volume_id) + mountpoint = bdm['device_name'] + connector = self.driver.get_volume_connector(instance) + volume = self.volume_api.get(context, new_volume_id) + try: + new_cinfo = self.volume_api.initialize_connection(context, + volume, + connector) + except Exception: # pylint: disable=W0702 + with excutils.save_and_reraise_exception(): + msg = _("Failed to connect to volume %(volume_id)s " + "with volume at %(mountpoint)s") + LOG.exception(msg % {'volume_id': new_volume_id, + 'mountpoint': mountpoint}, + context=context, + instance=instance) + self.volume_api.unreserve_volume(context, volume) + + old_cinfo = jsonutils.loads(bdm['connection_info']) + if old_cinfo and 'serial' not in old_cinfo: + old_cinfo['serial'] = old_volume_id + new_cinfo['serial'] = old_cinfo['serial'] + + try: + self.driver.swap_volume(old_cinfo, new_cinfo, instance, mountpoint) + except Exception: # pylint: disable=W0702 + with excutils.save_and_reraise_exception(): + msg = _("Failed to swap volume %(old_volume_id)s " + "for %(new_volume_id)s") + LOG.exception(msg % {'old_volume_id': old_volume_id, + 'new_volume_id': new_volume_id}, + context=context, + instance=instance) + self.volume_api.terminate_connection(context, + volume, + connector) + self.volume_api.attach(context, + volume, + instance['uuid'], + mountpoint) + # Remove old connection + volume = self.volume_api.get(context, old_volume_id) + self.volume_api.terminate_connection(context, volume, connector) + self.volume_api.detach(context.elevated(), volume) + # Update bdm + values = { + 'instance_uuid': instance['uuid'], + 'connection_info': jsonutils.dumps(new_cinfo), + 'device_name': mountpoint, + 'delete_on_termination': False, + 'virtual_name': None, + 'snapshot_id': None, + 'volume_id': new_volume_id, + 'volume_size': None, + 'no_device': None} + self.conductor_api.block_device_mapping_update_or_create(context, + values) + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def remove_volume_connection(self, context, volume_id, instance): """Remove a volume connection using the volume api.""" diff --git a/nova/compute/rpcapi.py b/nova/compute/rpcapi.py index d3c5b9027827..4f1aac83b985 100644 --- a/nova/compute/rpcapi.py +++ b/nova/compute/rpcapi.py @@ -186,6 +186,7 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy): 2.32 - Make reboot_instance take a new world instance object 2.33 - Made suspend_instance() and resume_instance() take new-world instance objects + 2.34 - Added swap_volume() ''' # @@ -600,6 +601,13 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy): return self.call(ctxt, self.make_msg('set_host_enabled', enabled=enabled), topic) + def swap_volume(self, ctxt, instance, old_volume_id, new_volume_id): + self.cast(ctxt, self.make_msg('swap_volume', + instance=instance, old_volume_id=old_volume_id, + new_volume_id=new_volume_id), + topic=_compute_topic(self.topic, ctxt, None, instance), + version='2.34') + def get_host_uptime(self, ctxt, host): topic = _compute_topic(self.topic, ctxt, host, None) return self.call(ctxt, self.make_msg('get_host_uptime'), topic) diff --git a/nova/tests/api/openstack/compute/contrib/test_volumes.py b/nova/tests/api/openstack/compute/contrib/test_volumes.py index bdca7fdeff16..24aa83ec85df 100644 --- a/nova/tests/api/openstack/compute/contrib/test_volumes.py +++ b/nova/tests/api/openstack/compute/contrib/test_volumes.py @@ -21,6 +21,7 @@ import webob from webob import exc from nova.api.openstack.compute.contrib import volumes +from nova.api.openstack import extensions from nova.compute import api as compute_api from nova.compute import flavors from nova import context @@ -65,7 +66,7 @@ def fake_compute_api_create(cls, context, instance_type, image_href, **kwargs): }], resv_id) -def fake_get_instance(self, context, instance_id): +def fake_get_instance(self, context, instance_id, want_objects=False): return {'uuid': instance_id} @@ -81,6 +82,11 @@ def fake_detach_volume(self, context, instance, volume): pass +def fake_swap_volume(self, context, instance, + old_volume_id, new_volume_id): + pass + + def fake_create_snapshot(self, context, volume, name, description): return {'id': 123, 'volume_id': 'fakeVolId', @@ -263,45 +269,43 @@ class VolumeAttachTests(test.TestCase): 'id': FAKE_UUID_A, 'volumeId': FAKE_UUID_A }} + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.attachments = volumes.VolumeAttachmentController(self.ext_mgr) def test_show(self): - attachments = volumes.VolumeAttachmentController() - req = webob.Request.blank('/v2/fake/os-volumes/show') + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') req.method = 'POST' req.body = jsonutils.dumps({}) req.headers['content-type'] = 'application/json' req.environ['nova.context'] = self.context - result = attachments.show(req, FAKE_UUID, FAKE_UUID_A) + result = self.attachments.show(req, FAKE_UUID, FAKE_UUID_A) self.assertEqual(self.expected_show, result) - def test_delete(self): + def test_detach(self): self.stubs.Set(compute_api.API, 'detach_volume', fake_detach_volume) - attachments = volumes.VolumeAttachmentController() - req = webob.Request.blank('/v2/fake/os-volumes/delete') - req.method = 'POST' - req.body = jsonutils.dumps({}) + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'DELETE' req.headers['content-type'] = 'application/json' req.environ['nova.context'] = self.context - result = attachments.delete(req, FAKE_UUID, FAKE_UUID_A) + result = self.attachments.delete(req, FAKE_UUID, FAKE_UUID_A) self.assertEqual('202 Accepted', result.status) - def test_delete_vol_not_found(self): + def test_detach_vol_not_found(self): self.stubs.Set(compute_api.API, 'detach_volume', fake_detach_volume) - attachments = volumes.VolumeAttachmentController() - req = webob.Request.blank('/v2/fake/os-volumes/delete') - req.method = 'POST' - req.body = jsonutils.dumps({}) + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'DELETE' req.headers['content-type'] = 'application/json' req.environ['nova.context'] = self.context self.assertRaises(exc.HTTPNotFound, - attachments.delete, + self.attachments.delete, req, FAKE_UUID, FAKE_UUID_C) @@ -310,15 +314,14 @@ class VolumeAttachTests(test.TestCase): self.stubs.Set(compute_api.API, 'attach_volume', fake_attach_volume) - attachments = volumes.VolumeAttachmentController() body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, 'device': '/dev/fake'}} - req = webob.Request.blank('/v2/fake/os-volumes/attach') + req = webob.Request.blank('/v2/servers/id/os-volume_attachments') req.method = 'POST' req.body = jsonutils.dumps({}) req.headers['content-type'] = 'application/json' req.environ['nova.context'] = self.context - result = attachments.create(req, FAKE_UUID, body) + result = self.attachments.create(req, FAKE_UUID, body) self.assertEqual(result['volumeAttachment']['id'], '00000000-aaaa-aaaa-aaaa-000000000000') @@ -326,7 +329,6 @@ class VolumeAttachTests(test.TestCase): self.stubs.Set(compute_api.API, 'attach_volume', fake_attach_volume) - attachments = volumes.VolumeAttachmentController() body = { 'volumeAttachment': { @@ -335,14 +337,41 @@ class VolumeAttachTests(test.TestCase): } } - req = fakes.HTTPRequest.blank('/v2/fake/os-volumes/attach') + req = webob.Request.blank('/v2/servers/id/os-volume_attachments') req.method = 'POST' - req.content_type = 'application/json' - req.body = jsonutils.dumps(body) + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context - self.assertRaises(webob.exc.HTTPBadRequest, attachments.create, + self.assertRaises(webob.exc.HTTPBadRequest, self.attachments.create, req, FAKE_UUID, body) + def _test_swap(self, uuid=FAKE_UUID_A): + self.stubs.Set(compute_api.API, + 'swap_volume', + fake_swap_volume) + body = {'volumeAttachment': {'volumeId': FAKE_UUID_B, + 'device': '/dev/fake'}} + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'PUT' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + return self.attachments.update(req, FAKE_UUID, uuid, body) + + def test_swap_volume_no_extension(self): + self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap) + + def test_swap_volume(self): + self.ext_mgr.extensions['os-volume-attachment-update'] = True + result = self._test_swap() + self.assertEqual('202 Accepted', result.status) + + def test_swap_volume_no_attachment(self): + self.ext_mgr.extensions['os-volume-attachment-update'] = True + + self.assertRaises(exc.HTTPNotFound, self._test_swap, FAKE_UUID_C) + class VolumeSerializerTest(test.TestCase): def _verify_volume_attachment(self, attach, tree): diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py index f5ecf3255494..2bedd69272da 100644 --- a/nova/tests/api/openstack/compute/test_extensions.py +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -229,6 +229,7 @@ class ExtensionControllerTest(ExtensionTestCase): "UsedLimits", "UserData", "VirtualInterfaces", + "VolumeAttachmentUpdate", "Volumes", ] self.ext_list.sort() diff --git a/nova/tests/compute/test_rpcapi.py b/nova/tests/compute/test_rpcapi.py index 2fb6068db252..48c78cc6b62f 100644 --- a/nova/tests/compute/test_rpcapi.py +++ b/nova/tests/compute/test_rpcapi.py @@ -229,6 +229,12 @@ class ComputeRpcAPITestCase(test.TestCase): reservations=['uuid1', 'uuid2'], version='2.27') + def test_swap_volume(self): + self._test_compute_api('swap_volume', 'cast', + instance=self.fake_instance, old_volume_id='oldid', + new_volume_id='newid', + version='2.34') + def test_restore_instance(self): self._test_compute_api('restore_instance', 'cast', instance=self.fake_instance) diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 4818769c1824..942fd0e4c8da 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -255,6 +255,7 @@ policy_data = """ "compute_extension:volume_attachments:index": "", "compute_extension:volume_attachments:show": "", "compute_extension:volume_attachments:create": "", + "compute_extension:volume_attachments:update": "", "compute_extension:volume_attachments:delete": "", "compute_extension:volumetypes": "", "compute_extension:zones": "", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index eca786e27470..c7019842b844 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -560,6 +560,14 @@ "namespace": "http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1", "updated": "%(timestamp)s" }, + { + "alias": "os-volume-attachment-update", + "description": "%(text)s", + "links": [], + "name": "VolumeAttachmentUpdate", + "namespace": "http://docs.openstack.org/compute/ext/os-volume-attachment-update/api/v2", + "updated": "%(timestamp)s" + }, { "alias": "os-volumes", "description": "%(text)s", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 8edf7f7e3d80..99896a82b527 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -210,6 +210,9 @@ %(text)s + + %(text)s + %(text)s diff --git a/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.json.tpl b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.json.tpl new file mode 100644 index 000000000000..d3916d1aa68a --- /dev/null +++ b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.json.tpl @@ -0,0 +1,16 @@ +{ + "server" : { + "name" : "new-server-test", + "imageRef" : "%(host)s/openstack/images/%(image_id)s", + "flavorRef" : "%(host)s/openstack/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + }, + "personality" : [ + { + "path" : "/etc/banner.txt", + "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.xml.tpl new file mode 100644 index 000000000000..f92614984242 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-req.xml.tpl @@ -0,0 +1,19 @@ + + + + Apache1 + + + + ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp + dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k + IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs + c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g + QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo + ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv + dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy + c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 + b25zLiINCg0KLVJpY2hhcmQgQmFjaA== + + + diff --git a/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.json.tpl new file mode 100644 index 000000000000..d5f030c8730b --- /dev/null +++ b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "server": { + "adminPass": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "%(host)s/v2/openstack/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(host)s/openstack/servers/%(uuid)s", + "rel": "bookmark" + } + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.xml.tpl new file mode 100644 index 000000000000..3bb13e69bd6d --- /dev/null +++ b/nova/tests/integrated/api_samples/os-volume-attachment-update/server-post-resp.xml.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.json.tpl b/nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.json.tpl new file mode 100644 index 000000000000..3d360a57bced --- /dev/null +++ b/nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.json.tpl @@ -0,0 +1,6 @@ +{ + "volumeAttachment": { + "volumeId": "%(volume_id)s", + "device": "%(device)s" + } +} diff --git a/nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.xml.tpl b/nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.xml.tpl new file mode 100644 index 000000000000..ffb20ad1eaf7 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-volume-attachment-update/update-volume-req.xml.tpl @@ -0,0 +1,2 @@ + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index c68630134510..45854737b413 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -3861,7 +3861,33 @@ class SnapshotsSampleXmlTests(SnapshotsSampleJsonTests): ctype = "xml" -class VolumeAttachmentsSampleJsonTest(ServersSampleBase): +class VolumeAttachmentsSampleBase(ServersSampleBase): + def _stub_compute_api_get_instance_bdms(self, server_id): + + def fake_compute_api_get_instance_bdms(self, context, instance): + bdms = [ + {'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f803', + 'instance_uuid': server_id, + 'device_name': '/dev/sdd'}, + {'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f804', + 'instance_uuid': server_id, + 'device_name': '/dev/sdc'} + ] + return bdms + + self.stubs.Set(compute_api.API, "get_instance_bdms", + fake_compute_api_get_instance_bdms) + + def _stub_compute_api_get(self): + + def fake_compute_api_get(self, context, instance_id, + want_objects=False): + return {'uuid': instance_id} + + self.stubs.Set(compute_api.API, 'get', fake_compute_api_get) + + +class VolumeAttachmentsSampleJsonTest(VolumeAttachmentsSampleBase): extension_name = ("nova.api.openstack.compute.contrib.volumes.Volumes") def test_attach_volume_to_server(self): @@ -3888,29 +3914,6 @@ class VolumeAttachmentsSampleJsonTest(ServersSampleBase): self._verify_response('attach-volume-to-server-resp', subs, response, 200) - def _stub_compute_api_get_instance_bdms(self, server_id): - - def fake_compute_api_get_instance_bdms(self, context, instance): - bdms = [ - {'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f803', - 'instance_uuid': server_id, - 'device_name': '/dev/sdd'}, - {'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f804', - 'instance_uuid': server_id, - 'device_name': '/dev/sdc'} - ] - return bdms - - self.stubs.Set(compute_api.API, "get_instance_bdms", - fake_compute_api_get_instance_bdms) - - def _stub_compute_api_get(self): - - def fake_compute_api_get(self, context, instance_id): - return {'uuid': instance_id} - - self.stubs.Set(compute_api.API, 'get', fake_compute_api_get) - def test_list_volume_attachments(self): server_id = self._post_server() @@ -3950,6 +3953,35 @@ class VolumeAttachmentsSampleXmlTest(VolumeAttachmentsSampleJsonTest): ctype = 'xml' +class VolumeAttachUpdateSampleJsonTest(VolumeAttachmentsSampleBase): + extends_name = ("nova.api.openstack.compute.contrib.volumes.Volumes") + extension_name = ("nova.api.openstack.compute.contrib." + "volume_attachment_update.Volume_attachment_update") + + def test_volume_attachment_update(self): + self.stubs.Set(cinder.API, 'get', fakes.stub_volume_get) + subs = { + 'volume_id': 'a26887c6-c47b-4654-abb5-dfadf7d3f805', + 'device': '/dev/sdd' + } + server_id = self._post_server() + attach_id = 'a26887c6-c47b-4654-abb5-dfadf7d3f803' + self._stub_compute_api_get_instance_bdms(server_id) + self._stub_compute_api_get() + self.stubs.Set(cinder.API, 'get', fakes.stub_volume_get) + self.stubs.Set(compute_api.API, 'swap_volume', lambda *a, **k: None) + response = self._do_put('servers/%s/os-volume_attachments/%s' + % (server_id, attach_id), + 'update-volume-req', + subs) + self.assertEqual(response.status, 202) + self.assertEqual(response.read(), '') + + +class VolumeAttachUpdateSampleXmlTest(VolumeAttachUpdateSampleJsonTest): + ctype = 'xml' + + class VolumesSampleJsonTest(ServersSampleBase): extension_name = ("nova.api.openstack.compute.contrib.volumes.Volumes") diff --git a/nova/tests/virt/test_virt_drivers.py b/nova/tests/virt/test_virt_drivers.py index b7a9b90aba70..5f390e34a556 100644 --- a/nova/tests/virt/test_virt_drivers.py +++ b/nova/tests/virt/test_virt_drivers.py @@ -418,6 +418,17 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase): instance_ref, '/dev/sda') + @catch_notimplementederror + def test_swap_volume(self): + instance_ref, network_info = self._get_running_instance() + self.connection.attach_volume({'driver_volume_type': 'fake'}, + instance_ref, + '/dev/sda') + self.connection.swap_volume({'driver_volume_type': 'fake'}, + {'driver_volume_type': 'fake'}, + instance_ref, + '/dev/sda') + @catch_notimplementederror def test_attach_detach_different_power_states(self): instance_ref, network_info = self._get_running_instance() diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 400428e65ce3..3f83dc1dd68f 100755 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -328,6 +328,11 @@ class ComputeDriver(object): """Detach the disk attached to the instance.""" raise NotImplementedError() + def swap_volume(self, old_connection_info, new_connection_info, + instance, mountpoint): + """Replace the disk attached to the instance.""" + raise NotImplementedError() + def attach_interface(self, instance, image_meta, network_info): """Attach an interface to the instance.""" raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 8ef83dc8c3bf..95f08efa874b 100755 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -234,6 +234,15 @@ class FakeDriver(driver.ComputeDriver): pass return True + def swap_volume(self, old_connection_info, new_connection_info, + instance, mountpoint): + """Replace the disk attached to the instance.""" + instance_name = instance['name'] + if instance_name not in self._mounts: + self._mounts[instance_name] = {} + self._mounts[instance_name][mountpoint] = new_connection_info + return True + def attach_interface(self, instance, image_meta, network_info): for (network, mapping) in network_info: if mapping['vif_uuid'] in self._interfaces: diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 3517802a9885..53e5510da51b 100755 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -1044,6 +1044,65 @@ class LibvirtDriver(driver.ComputeDriver): connection_info, disk_dev) + def _swap_volume(self, domain, disk_path, new_path): + """Swap existing disk with a new block device.""" + # Save a copy of the domain's running XML file + xml = domain.XMLDesc(0) + + # Abort is an idempotent operation, so make sure any block + # jobs which may have failed are ended. + try: + domain.blockJobAbort(disk_path, 0) + except Exception: + pass + + try: + # NOTE (rmk): blockRebase cannot be executed on persistent + # domains, so we need to temporarily undefine it. + # If any part of this block fails, the domain is + # re-defined regardless. + if domain.isPersistent(): + domain.undefine() + + domain.blockRebase(disk_path, new_path, 0, + libvirt.VIR_DOMAIN_BLOCK_REBASE_COPY) + + while self._wait_for_block_job(domain, disk_path): + time.sleep(0.5) + + domain.blockJobAbort(disk_path, + libvirt.VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT) + finally: + self._conn.defineXML(xml) + + def swap_volume(self, old_connection_info, + new_connection_info, instance, mountpoint): + instance_name = instance['name'] + virt_dom = self._lookup_by_name(instance_name) + disk_dev = mountpoint.rpartition("/")[2] + xml = self._get_disk_xml(virt_dom.XMLDesc(0), disk_dev) + if not xml: + raise exception.DiskNotFound(location=disk_dev) + disk_info = { + 'dev': disk_dev, + 'bus': blockinfo.get_disk_bus_for_disk_dev(CONF.libvirt_type, + disk_dev), + 'type': 'disk', + } + conf = self.volume_driver_method('connect_volume', + new_connection_info, + disk_info) + if not conf.source_path: + self.volume_driver_method('disconnect_volume', + new_connection_info, + disk_dev) + raise NotImplementedError(_("Swap only supports host devices")) + + self._swap_volume(virt_dom, disk_dev, conf.source_path) + self.volume_driver_method('disconnect_volume', + old_connection_info, + disk_dev) + @staticmethod def _get_disk_xml(xml, device): """Returns the xml for the disk mounted at device.""" @@ -1288,23 +1347,31 @@ class LibvirtDriver(driver.ComputeDriver): LOG.info(_("Snapshot image upload complete"), instance=instance) + @staticmethod + def _wait_for_block_job(domain, disk_path): + status = domain.blockJobInfo(disk_path, 0) + try: + cur = status.get('cur', 0) + end = status.get('end', 0) + except Exception: + return False + + if cur == end and cur != 0 and end != 0: + return False + else: + return True + def _live_snapshot(self, domain, disk_path, out_path, image_format): """Snapshot an instance without downtime.""" # Save a copy of the domain's running XML file xml = domain.XMLDesc(0) - def _wait_for_block_job(domain, disk_path): - status = domain.blockJobInfo(disk_path, 0) - try: - cur = status.get('cur', 0) - end = status.get('end', 0) - except Exception: - return False - - if cur == end and cur != 0 and end != 0: - return False - else: - return True + # Abort is an idempotent operation, so make sure any block + # jobs which may have failed are ended. + try: + domain.blockJobAbort(disk_path, 0) + except Exception: + pass # NOTE (rmk): We are using shallow rebases as a workaround to a bug # in QEMU 1.3. In order to do this, we need to create @@ -1332,7 +1399,7 @@ class LibvirtDriver(driver.ComputeDriver): libvirt.VIR_DOMAIN_BLOCK_REBASE_REUSE_EXT | libvirt.VIR_DOMAIN_BLOCK_REBASE_SHALLOW) - while _wait_for_block_job(domain, disk_path): + while self._wait_for_block_job(domain, disk_path): time.sleep(0.5) domain.blockJobAbort(disk_path, 0)