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:
Vishvananda Ishaya 2013-05-13 12:36:56 -07:00
parent 17baddcebf
commit 8f51b120b4
31 changed files with 603 additions and 113 deletions

View File

@ -96,14 +96,6 @@
"namespace": "http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1", "namespace": "http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1",
"updated": "2011-09-14T00:00:00+00:00" "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", "alias": "OS-SCH-HNT",
"description": "Pass arbitrary key/value pairs to the scheduler.", "description": "Pass arbitrary key/value pairs to the scheduler.",
@ -112,6 +104,14 @@
"namespace": "http://docs.openstack.org/compute/ext/scheduler-hints/api/v2", "namespace": "http://docs.openstack.org/compute/ext/scheduler-hints/api/v2",
"updated": "2011-07-19T00:00:00+00:00" "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", "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 ", "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", "namespace": "http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2",
"updated": "2013-01-04T00:00:00+00:00" "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", "alias": "os-cell-capacities",
"description": "Adding functionality to get 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", "namespace": "http://docs.openstack.org/compute/ext/cell_capacities/api/v1.1",
"updated": "2013-05-27T00:00:00+00:00" "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", "alias": "os-certificates",
"description": "Certificates support.", "description": "Certificates support.",
@ -264,6 +264,22 @@
"namespace": "http://docs.openstack.org/compute/ext/extended_floating_ips/api/v2", "namespace": "http://docs.openstack.org/compute/ext/extended_floating_ips/api/v2",
"updated": "2013-04-19T00:00:00+00:00" "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", "alias": "os-fixed-ips",
"description": "Fixed IPs support.", "description": "Fixed IPs support.",
@ -432,14 +448,6 @@
"namespace": "http://docs.openstack.org/compute/ext/quota-classes-sets/api/v1.1", "namespace": "http://docs.openstack.org/compute/ext/quota-classes-sets/api/v1.1",
"updated": "2012-03-12T00:00:00+00:00" "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", "alias": "os-quota-sets",
"description": "Quotas management support.", "description": "Quotas management support.",
@ -470,7 +478,7 @@
"links": [], "links": [],
"name": "SecurityGroups", "name": "SecurityGroups",
"namespace": "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1", "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", "alias": "os-server-diagnostics",
@ -504,14 +512,6 @@
"namespace": "http://docs.openstack.org/compute/ext/services/api/v2", "namespace": "http://docs.openstack.org/compute/ext/services/api/v2",
"updated": "2012-10-28T00:00:00-00:00" "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", "alias": "os-shelve",
"description": "Instance shelve mode.", "description": "Instance shelve mode.",
@ -568,6 +568,14 @@
"namespace": "http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1", "namespace": "http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1",
"updated": "2011-08-17T00:00:00+00:00" "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", "alias": "os-volumes",
"description": "Volumes support.", "description": "Volumes support.",

View File

@ -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"> <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> <description>Provide additional data for flavors.</description>
</extension> </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"> <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> <description>Pass arbitrary key/value pairs to the scheduler.</description>
</extension> </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"> <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 <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"> <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> <description>Admin-only bare-metal node administration.</description>
</extension> </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, <description>Enables cells-related functionality such as adding neighbor cells,
listing neighbor cells, and getting the capabilities of the local cell. listing neighbor cells, and getting the capabilities of the local cell.
</description> </description>
</extension> </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"> <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> <description>Certificates support.</description>
</extension> </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"> <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> <description>Adds optional fixed_address to the add floating IP command.</description>
</extension> </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"> <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> <description>Fixed IPs support.</description>
</extension> </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"> <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> <description>Quota classes management support.</description>
</extension> </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"> <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> <description>Quotas management support.</description>
</extension> </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"> <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> <description>Default rules for security group support.</description>
</extension> </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> <description>Security group support.</description>
</extension> </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"> <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"> <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> <description>Services support.</description>
</extension> </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"> <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> <description>Instance shelve mode.</description>
</extension> </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"> <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> <description>Virtual interface support.</description>
</extension> </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"> <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> <description>Volumes support.</description>
</extension> </extension>

View File

@ -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=="
}
]
}
}

View File

@ -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>

View File

@ -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"
}
]
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
{
"volumeAttachment": {
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f805",
"device": "/dev/sdd"
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<volumeAttachment volumeId="a26887c6-c47b-4654-abb5-dfadf7d3f805" device="/dev/sdd" />

View File

@ -179,6 +179,7 @@
"compute_extension:volume_attachments:index": "", "compute_extension:volume_attachments:index": "",
"compute_extension:volume_attachments:show": "", "compute_extension:volume_attachments:show": "",
"compute_extension:volume_attachments:create": "", "compute_extension:volume_attachments:create": "",
"compute_extension:volume_attachments:update": "",
"compute_extension:volume_attachments:delete": "", "compute_extension:volume_attachments:delete": "",
"compute_extension:volumetypes": "", "compute_extension:volumetypes": "",
"compute_extension:availability_zone:list": "", "compute_extension:availability_zone:list": "",

View File

@ -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"

View File

@ -326,9 +326,10 @@ class VolumeAttachmentController(wsgi.Controller):
""" """
def __init__(self): def __init__(self, ext_mgr=None):
self.compute_api = compute.API() self.compute_api = compute.API()
self.volume_api = volume.API() self.volume_api = volume.API()
self.ext_mgr = ext_mgr
super(VolumeAttachmentController, self).__init__() super(VolumeAttachmentController, self).__init__()
@wsgi.serializers(xml=VolumeAttachmentsTemplate) @wsgi.serializers(xml=VolumeAttachmentsTemplate)
@ -431,8 +432,52 @@ class VolumeAttachmentController(wsgi.Controller):
return {'volumeAttachment': attachment} return {'volumeAttachment': attachment}
def update(self, req, server_id, id, body): def update(self, req, server_id, id, body):
"""Update a volume attachment. We don't currently support this.""" if (not self.ext_mgr or
raise exc.HTTPBadRequest() 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): def delete(self, req, server_id, id):
"""Detach a volume from an instance.""" """Detach a volume from an instance."""
@ -656,8 +701,9 @@ class Volumes(extensions.ExtensionDescriptor):
collection_actions={'detail': 'GET'}) collection_actions={'detail': 'GET'})
resources.append(res) resources.append(res)
attachment_controller = VolumeAttachmentController(self.ext_mgr)
res = extensions.ResourceExtension('os-volume_attachments', res = extensions.ResourceExtension('os-volume_attachments',
VolumeAttachmentController(), attachment_controller,
parent=dict( parent=dict(
member_name='server', member_name='server',
collection_name='servers')) collection_name='servers'))

View File

@ -2551,6 +2551,36 @@ class API(base.Base):
raise exception.VolumeUnattached(volume_id=volume['id']) raise exception.VolumeUnattached(volume_id=volume['id'])
self._detach_volume(context, instance, volume) 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 @wrap_check_policy
def attach_interface(self, context, instance, network_id, port_id, def attach_interface(self, context, instance, network_id, port_id,
requested_ip): requested_ip):

View File

@ -364,7 +364,7 @@ class ComputeVirtAPI(virtapi.VirtAPI):
class ComputeManager(manager.SchedulerDependentManager): class ComputeManager(manager.SchedulerDependentManager):
"""Manages the running instances from creation to destruction.""" """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): def __init__(self, compute_driver=None, *args, **kwargs):
"""Load configuration options and connect to the hypervisor.""" """Load configuration options and connect to the hypervisor."""
@ -3524,6 +3524,70 @@ class ComputeManager(manager.SchedulerDependentManager):
self._notify_about_instance_usage( self._notify_about_instance_usage(
context, instance, "volume.detach", extra_usage_info=info) 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()) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
def remove_volume_connection(self, context, volume_id, instance): def remove_volume_connection(self, context, volume_id, instance):
"""Remove a volume connection using the volume api.""" """Remove a volume connection using the volume api."""

View File

@ -186,6 +186,7 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy):
2.32 - Make reboot_instance take a new world instance object 2.32 - Make reboot_instance take a new world instance object
2.33 - Made suspend_instance() and resume_instance() take new-world 2.33 - Made suspend_instance() and resume_instance() take new-world
instance objects 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', return self.call(ctxt, self.make_msg('set_host_enabled',
enabled=enabled), topic) 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): def get_host_uptime(self, ctxt, host):
topic = _compute_topic(self.topic, ctxt, host, None) topic = _compute_topic(self.topic, ctxt, host, None)
return self.call(ctxt, self.make_msg('get_host_uptime'), topic) return self.call(ctxt, self.make_msg('get_host_uptime'), topic)

View File

@ -21,6 +21,7 @@ import webob
from webob import exc from webob import exc
from nova.api.openstack.compute.contrib import volumes 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 api as compute_api
from nova.compute import flavors from nova.compute import flavors
from nova import context from nova import context
@ -65,7 +66,7 @@ def fake_compute_api_create(cls, context, instance_type, image_href, **kwargs):
}], resv_id) }], 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} return {'uuid': instance_id}
@ -81,6 +82,11 @@ def fake_detach_volume(self, context, instance, volume):
pass pass
def fake_swap_volume(self, context, instance,
old_volume_id, new_volume_id):
pass
def fake_create_snapshot(self, context, volume, name, description): def fake_create_snapshot(self, context, volume, name, description):
return {'id': 123, return {'id': 123,
'volume_id': 'fakeVolId', 'volume_id': 'fakeVolId',
@ -263,45 +269,43 @@ class VolumeAttachTests(test.TestCase):
'id': FAKE_UUID_A, 'id': FAKE_UUID_A,
'volumeId': 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): def test_show(self):
attachments = volumes.VolumeAttachmentController() req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
req = webob.Request.blank('/v2/fake/os-volumes/show')
req.method = 'POST' req.method = 'POST'
req.body = jsonutils.dumps({}) req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json' req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context 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) self.assertEqual(self.expected_show, result)
def test_delete(self): def test_detach(self):
self.stubs.Set(compute_api.API, self.stubs.Set(compute_api.API,
'detach_volume', 'detach_volume',
fake_detach_volume) fake_detach_volume)
attachments = volumes.VolumeAttachmentController() req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
req = webob.Request.blank('/v2/fake/os-volumes/delete') req.method = 'DELETE'
req.method = 'POST'
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json' req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context 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) 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, self.stubs.Set(compute_api.API,
'detach_volume', 'detach_volume',
fake_detach_volume) fake_detach_volume)
attachments = volumes.VolumeAttachmentController() req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
req = webob.Request.blank('/v2/fake/os-volumes/delete') req.method = 'DELETE'
req.method = 'POST'
req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json' req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context req.environ['nova.context'] = self.context
self.assertRaises(exc.HTTPNotFound, self.assertRaises(exc.HTTPNotFound,
attachments.delete, self.attachments.delete,
req, req,
FAKE_UUID, FAKE_UUID,
FAKE_UUID_C) FAKE_UUID_C)
@ -310,15 +314,14 @@ class VolumeAttachTests(test.TestCase):
self.stubs.Set(compute_api.API, self.stubs.Set(compute_api.API,
'attach_volume', 'attach_volume',
fake_attach_volume) fake_attach_volume)
attachments = volumes.VolumeAttachmentController()
body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, body = {'volumeAttachment': {'volumeId': FAKE_UUID_A,
'device': '/dev/fake'}} '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.method = 'POST'
req.body = jsonutils.dumps({}) req.body = jsonutils.dumps({})
req.headers['content-type'] = 'application/json' req.headers['content-type'] = 'application/json'
req.environ['nova.context'] = self.context 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'], self.assertEqual(result['volumeAttachment']['id'],
'00000000-aaaa-aaaa-aaaa-000000000000') '00000000-aaaa-aaaa-aaaa-000000000000')
@ -326,7 +329,6 @@ class VolumeAttachTests(test.TestCase):
self.stubs.Set(compute_api.API, self.stubs.Set(compute_api.API,
'attach_volume', 'attach_volume',
fake_attach_volume) fake_attach_volume)
attachments = volumes.VolumeAttachmentController()
body = { body = {
'volumeAttachment': { '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.method = 'POST'
req.content_type = 'application/json' req.body = jsonutils.dumps({})
req.body = jsonutils.dumps(body) 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) 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): class VolumeSerializerTest(test.TestCase):
def _verify_volume_attachment(self, attach, tree): def _verify_volume_attachment(self, attach, tree):

View File

@ -229,6 +229,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"UsedLimits", "UsedLimits",
"UserData", "UserData",
"VirtualInterfaces", "VirtualInterfaces",
"VolumeAttachmentUpdate",
"Volumes", "Volumes",
] ]
self.ext_list.sort() self.ext_list.sort()

View File

@ -229,6 +229,12 @@ class ComputeRpcAPITestCase(test.TestCase):
reservations=['uuid1', 'uuid2'], reservations=['uuid1', 'uuid2'],
version='2.27') 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): def test_restore_instance(self):
self._test_compute_api('restore_instance', 'cast', self._test_compute_api('restore_instance', 'cast',
instance=self.fake_instance) instance=self.fake_instance)

View File

@ -255,6 +255,7 @@ policy_data = """
"compute_extension:volume_attachments:index": "", "compute_extension:volume_attachments:index": "",
"compute_extension:volume_attachments:show": "", "compute_extension:volume_attachments:show": "",
"compute_extension:volume_attachments:create": "", "compute_extension:volume_attachments:create": "",
"compute_extension:volume_attachments:update": "",
"compute_extension:volume_attachments:delete": "", "compute_extension:volume_attachments:delete": "",
"compute_extension:volumetypes": "", "compute_extension:volumetypes": "",
"compute_extension:zones": "", "compute_extension:zones": "",

View File

@ -560,6 +560,14 @@
"namespace": "http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1", "namespace": "http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1",
"updated": "%(timestamp)s" "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", "alias": "os-volumes",
"description": "%(text)s", "description": "%(text)s",

View File

@ -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"> <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> <description>%(text)s</description>
</extension> </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"> <extension alias="os-volumes" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes">
<description>%(text)s</description> <description>%(text)s</description>
</extension> </extension>

View File

@ -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=="
}
]
}
}

View File

@ -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>

View File

@ -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"
}
]
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
{
"volumeAttachment": {
"volumeId": "%(volume_id)s",
"device": "%(device)s"
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<volumeAttachment volumeId="%(volume_id)s" device="%(device)s" />

View File

@ -3861,7 +3861,33 @@ class SnapshotsSampleXmlTests(SnapshotsSampleJsonTests):
ctype = "xml" 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") extension_name = ("nova.api.openstack.compute.contrib.volumes.Volumes")
def test_attach_volume_to_server(self): def test_attach_volume_to_server(self):
@ -3888,29 +3914,6 @@ class VolumeAttachmentsSampleJsonTest(ServersSampleBase):
self._verify_response('attach-volume-to-server-resp', subs, self._verify_response('attach-volume-to-server-resp', subs,
response, 200) 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): def test_list_volume_attachments(self):
server_id = self._post_server() server_id = self._post_server()
@ -3950,6 +3953,35 @@ class VolumeAttachmentsSampleXmlTest(VolumeAttachmentsSampleJsonTest):
ctype = 'xml' 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): class VolumesSampleJsonTest(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib.volumes.Volumes") extension_name = ("nova.api.openstack.compute.contrib.volumes.Volumes")

View File

@ -418,6 +418,17 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase):
instance_ref, instance_ref,
'/dev/sda') '/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 @catch_notimplementederror
def test_attach_detach_different_power_states(self): def test_attach_detach_different_power_states(self):
instance_ref, network_info = self._get_running_instance() instance_ref, network_info = self._get_running_instance()

View File

@ -328,6 +328,11 @@ class ComputeDriver(object):
"""Detach the disk attached to the instance.""" """Detach the disk attached to the instance."""
raise NotImplementedError() 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): def attach_interface(self, instance, image_meta, network_info):
"""Attach an interface to the instance.""" """Attach an interface to the instance."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -234,6 +234,15 @@ class FakeDriver(driver.ComputeDriver):
pass pass
return True 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): def attach_interface(self, instance, image_meta, network_info):
for (network, mapping) in network_info: for (network, mapping) in network_info:
if mapping['vif_uuid'] in self._interfaces: if mapping['vif_uuid'] in self._interfaces:

View File

@ -1044,6 +1044,65 @@ class LibvirtDriver(driver.ComputeDriver):
connection_info, connection_info,
disk_dev) 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 @staticmethod
def _get_disk_xml(xml, device): def _get_disk_xml(xml, device):
"""Returns the xml for the disk mounted at device.""" """Returns the xml for the disk mounted at device."""
@ -1288,23 +1347,31 @@ class LibvirtDriver(driver.ComputeDriver):
LOG.info(_("Snapshot image upload complete"), LOG.info(_("Snapshot image upload complete"),
instance=instance) 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): def _live_snapshot(self, domain, disk_path, out_path, image_format):
"""Snapshot an instance without downtime.""" """Snapshot an instance without downtime."""
# Save a copy of the domain's running XML file # Save a copy of the domain's running XML file
xml = domain.XMLDesc(0) xml = domain.XMLDesc(0)
def _wait_for_block_job(domain, disk_path): # Abort is an idempotent operation, so make sure any block
status = domain.blockJobInfo(disk_path, 0) # jobs which may have failed are ended.
try: try:
cur = status.get('cur', 0) domain.blockJobAbort(disk_path, 0)
end = status.get('end', 0) except Exception:
except Exception: pass
return False
if cur == end and cur != 0 and end != 0:
return False
else:
return True
# NOTE (rmk): We are using shallow rebases as a workaround to a bug # 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 # 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_REUSE_EXT |
libvirt.VIR_DOMAIN_BLOCK_REBASE_SHALLOW) 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) time.sleep(0.5)
domain.blockJobAbort(disk_path, 0) domain.blockJobAbort(disk_path, 0)