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)