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
This commit is contained in:
parent
17baddcebf
commit
8f51b120b4
@ -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.",
|
||||
|
@ -36,12 +36,12 @@
|
||||
<extension alias="OS-FLV-EXT-DATA" updated="2011-09-14T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1" name="FlavorExtraData">
|
||||
<description>Provide additional data for flavors.</description>
|
||||
</extension>
|
||||
<extension alias="OS-SRV-USG" updated="2013-04-29T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/server_usage/api/v1.1" name="ServerUsage">
|
||||
<description>Adds launched_at and terminated_at on Servers.</description>
|
||||
</extension>
|
||||
<extension alias="OS-SCH-HNT" updated="2011-07-19T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/scheduler-hints/api/v2" name="SchedulerHints">
|
||||
<description>Pass arbitrary key/value pairs to the scheduler.</description>
|
||||
</extension>
|
||||
<extension alias="OS-SRV-USG" updated="2013-04-29T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/server_usage/api/v1.1" name="ServerUsage">
|
||||
<description>Adds launched_at and terminated_at on Servers.</description>
|
||||
</extension>
|
||||
<extension alias="os-admin-actions" updated="2011-09-20T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/admin-actions/api/v1.1" name="AdminActions">
|
||||
<description>Enable admin-only server actions
|
||||
|
||||
@ -66,14 +66,14 @@
|
||||
<extension alias="os-baremetal-nodes" updated="2013-01-04T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2" name="BareMetalNodes">
|
||||
<description>Admin-only bare-metal node administration.</description>
|
||||
</extension>
|
||||
<extension alias="os-cells" updated="2011-09-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
|
||||
<extension alias="os-cell-capacities" updated="2013-05-27T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cell_capacities/api/v1.1" name="CellCapacities">
|
||||
<description>Adding functionality to get cell capacities.</description>
|
||||
</extension>
|
||||
<extension alias="os-cells" updated="2013-05-14T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
|
||||
<description>Enables cells-related functionality such as adding neighbor cells,
|
||||
listing neighbor cells, and getting the capabilities of the local cell.
|
||||
</description>
|
||||
</extension>
|
||||
<extension alias="os-cell-capacities" updated="2013-05-27T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cell_capacities/api/v1.1" name="CellCapacities">
|
||||
<description>Adds functionality to get cell capacities.</description>
|
||||
</extension>
|
||||
<extension alias="os-certificates" updated="2012-01-19T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/certificates/api/v1.1" name="Certificates">
|
||||
<description>Certificates support.</description>
|
||||
</extension>
|
||||
@ -115,6 +115,14 @@
|
||||
<extension alias="os-extended-floating-ips" updated="2013-04-19T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended_floating_ips/api/v2" name="ExtendedFloatingIps">
|
||||
<description>Adds optional fixed_address to the add floating IP command.</description>
|
||||
</extension>
|
||||
<extension alias="os-extended-quotas" updated="2013-06-09T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended_quotas/api/v1.1" name="ExtendedQuotas">
|
||||
<description>Adds ability for admins to delete quota
|
||||
and optionally force the update Quota command.
|
||||
</description>
|
||||
</extension>
|
||||
<extension alias="os-extended-services" updated="2013-05-17T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/extended_services/api/v2" name="ExtendedServices">
|
||||
<description>Extended services support.</description>
|
||||
</extension>
|
||||
<extension alias="os-fixed-ips" updated="2012-10-18T13:25:27-06:00" namespace="http://docs.openstack.org/compute/ext/fixed_ips/api/v2" name="FixedIPs">
|
||||
<description>Fixed IPs support.</description>
|
||||
</extension>
|
||||
@ -180,9 +188,6 @@
|
||||
<extension alias="os-quota-class-sets" updated="2012-03-12T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/quota-classes-sets/api/v1.1" name="QuotaClasses">
|
||||
<description>Quota classes management support.</description>
|
||||
</extension>
|
||||
<extension alias="os-extended-quotas" updated="2013-06-09T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended_quotas/api/v1.1" name="ExtendedQuotas">
|
||||
<description>Adds ability for admins to delete quota and optionally force the update Quota command.</description>
|
||||
</extension>
|
||||
<extension alias="os-quota-sets" updated="2011-08-08T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" name="Quotas">
|
||||
<description>Quotas management support.</description>
|
||||
</extension>
|
||||
@ -192,7 +197,7 @@
|
||||
<extension alias="os-security-group-default-rules" updated="2013-02-05T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/securitygroupdefaultrules/api/v1.1" name="SecurityGroupDefaultRules">
|
||||
<description>Default rules for security group support.</description>
|
||||
</extension>
|
||||
<extension alias="os-security-groups" updated="2011-07-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/securitygroups/api/v1.1" name="SecurityGroups">
|
||||
<extension alias="os-security-groups" updated="2013-05-28T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/securitygroups/api/v1.1" name="SecurityGroups">
|
||||
<description>Security group support.</description>
|
||||
</extension>
|
||||
<extension alias="os-server-diagnostics" updated="2011-12-21T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/server-diagnostics/api/v1.1" name="ServerDiagnostics">
|
||||
@ -207,9 +212,6 @@
|
||||
<extension alias="os-services" updated="2012-10-28T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/services/api/v2" name="Services">
|
||||
<description>Services support.</description>
|
||||
</extension>
|
||||
<extension alias="os-extended-services" updated="2013-05-17T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/extended_services/api/v2" name="ExtendedServices">
|
||||
<description>Extended services support.</description>
|
||||
</extension>
|
||||
<extension alias="os-shelve" updated="2013-04-06T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/shelve/api/v1.1" name="Shelve">
|
||||
<description>Instance shelve mode.</description>
|
||||
</extension>
|
||||
@ -231,6 +233,9 @@
|
||||
<extension alias="os-virtual-interfaces" updated="2011-08-17T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1" name="VirtualInterfaces">
|
||||
<description>Virtual interface support.</description>
|
||||
</extension>
|
||||
<extension alias="os-volume-attachment-update" updated="2013-06-20T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/os-volume-attachment-update/api/v2" name="VolumeAttachmentUpdate">
|
||||
<description>Support for updating a volume attachment.</description>
|
||||
</extension>
|
||||
<extension alias="os-volumes" updated="2011-03-25T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes">
|
||||
<description>Volumes support.</description>
|
||||
</extension>
|
||||
|
@ -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=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b" flavorRef="http://openstack.example.com/openstack/flavors/1" name="new-server-test">
|
||||
<metadata>
|
||||
<meta key="My Server Name">Apache1</meta>
|
||||
</metadata>
|
||||
<personality>
|
||||
<file path="/etc/banner.txt">
|
||||
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
|
||||
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
|
||||
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
|
||||
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
|
||||
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
|
||||
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
|
||||
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
|
||||
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
|
||||
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
|
||||
</file>
|
||||
</personality>
|
||||
</server>
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="611a0c5d-64ec-4d6a-a6ed-56ccf90921c6" adminPass="RoXt8zgxNxAR">
|
||||
<metadata/>
|
||||
<atom:link href="http://openstack.example.com/v2/openstack/servers/611a0c5d-64ec-4d6a-a6ed-56ccf90921c6" rel="self"/>
|
||||
<atom:link href="http://openstack.example.com/openstack/servers/611a0c5d-64ec-4d6a-a6ed-56ccf90921c6" rel="bookmark"/>
|
||||
</server>
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f805",
|
||||
"device": "/dev/sdd"
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<volumeAttachment volumeId="a26887c6-c47b-4654-abb5-dfadf7d3f805" device="/dev/sdd" />
|
@ -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": "",
|
||||
|
@ -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"
|
@ -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'))
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -229,6 +229,7 @@ class ExtensionControllerTest(ExtensionTestCase):
|
||||
"UsedLimits",
|
||||
"UserData",
|
||||
"VirtualInterfaces",
|
||||
"VolumeAttachmentUpdate",
|
||||
"Volumes",
|
||||
]
|
||||
self.ext_list.sort()
|
||||
|
@ -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)
|
||||
|
@ -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": "",
|
||||
|
@ -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",
|
||||
|
@ -210,6 +210,9 @@
|
||||
<extension alias="os-virtual-interfaces" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1" name="VirtualInterfaces">
|
||||
<description>%(text)s</description>
|
||||
</extension>
|
||||
<extension alias="os-volume-attachment-update" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/os-volume-attachment-update/api/v2" name="VolumeAttachmentUpdate">
|
||||
<description>%(text)s</description>
|
||||
</extension>
|
||||
<extension alias="os-volumes" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes">
|
||||
<description>%(text)s</description>
|
||||
</extension>
|
||||
|
@ -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=="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="%(host)s/openstack/images/%(image_id)s" flavorRef="%(host)s/openstack/flavors/1" name="new-server-test">
|
||||
<metadata>
|
||||
<meta key="My Server Name">Apache1</meta>
|
||||
</metadata>
|
||||
<personality>
|
||||
<file path="/etc/banner.txt">
|
||||
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
|
||||
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
|
||||
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
|
||||
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
|
||||
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
|
||||
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
|
||||
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
|
||||
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
|
||||
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
|
||||
</file>
|
||||
</personality>
|
||||
</server>
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="%(id)s" adminPass="%(password)s">
|
||||
<metadata/>
|
||||
<atom:link href="%(host)s/v2/openstack/servers/%(uuid)s" rel="self"/>
|
||||
<atom:link href="%(host)s/openstack/servers/%(uuid)s" rel="bookmark"/>
|
||||
</server>
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "%(volume_id)s",
|
||||
"device": "%(device)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<volumeAttachment volumeId="%(volume_id)s" device="%(device)s" />
|
@ -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")
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user