diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json
index 3caa0e893bfd..a52c8fd9099e 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.json
+++ b/doc/api_samples/all_extensions/extensions-get-resp.json
@@ -512,6 +512,14 @@
"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.",
+ "links": [],
+ "name": "Shelve",
+ "namespace": "http://docs.openstack.org/compute/ext/shelve/api/v1.1",
+ "updated": "2013-04-06T00:00:00+00:00"
+ },
{
"alias": "os-simple-tenant-usage",
"description": "Simple tenant usage extension.",
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml
index fd4fed4b1b45..5cac81097d49 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.xml
+++ b/doc/api_samples/all_extensions/extensions-get-resp.xml
@@ -210,6 +210,9 @@
Extended services support.
+
+ Instance shelve mode.
+
Simple tenant usage extension.
diff --git a/doc/api_samples/os-shelve/os-shelve.json b/doc/api_samples/os-shelve/os-shelve.json
new file mode 100644
index 000000000000..e33b05865aca
--- /dev/null
+++ b/doc/api_samples/os-shelve/os-shelve.json
@@ -0,0 +1,3 @@
+{
+ "shelve": null
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-shelve/os-shelve.xml b/doc/api_samples/os-shelve/os-shelve.xml
new file mode 100644
index 000000000000..687b1b1f58c8
--- /dev/null
+++ b/doc/api_samples/os-shelve/os-shelve.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/doc/api_samples/os-shelve/server-post-req.json b/doc/api_samples/os-shelve/server-post-req.json
new file mode 100644
index 000000000000..d88eb4122223
--- /dev/null
+++ b/doc/api_samples/os-shelve/server-post-req.json
@@ -0,0 +1,16 @@
+{
+ "server" : {
+ "name" : "new-server-test",
+ "imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
+ "flavorRef" : "http://openstack.example.com/openstack/flavors/1",
+ "metadata" : {
+ "My Server Name" : "Apache1"
+ },
+ "personality" : [
+ {
+ "path" : "/etc/banner.txt",
+ "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-shelve/server-post-req.xml b/doc/api_samples/os-shelve/server-post-req.xml
new file mode 100644
index 000000000000..0a3c8bb5303d
--- /dev/null
+++ b/doc/api_samples/os-shelve/server-post-req.xml
@@ -0,0 +1,19 @@
+
+
+
+ Apache1
+
+
+
+ ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
+ dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
+ IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
+ c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
+ QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
+ ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
+ dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
+ c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
+ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
+
+
+
\ No newline at end of file
diff --git a/doc/api_samples/os-shelve/server-post-resp.json b/doc/api_samples/os-shelve/server-post-resp.json
new file mode 100644
index 000000000000..23de4473f9a1
--- /dev/null
+++ b/doc/api_samples/os-shelve/server-post-resp.json
@@ -0,0 +1,16 @@
+{
+ "server": {
+ "adminPass": "bGZzzzeaSp9z",
+ "id": "9582b762-0964-4509-8fff-0146c02abe31",
+ "links": [
+ {
+ "href": "http://openstack.example.com/v2/openstack/servers/9582b762-0964-4509-8fff-0146c02abe31",
+ "rel": "self"
+ },
+ {
+ "href": "http://openstack.example.com/openstack/servers/9582b762-0964-4509-8fff-0146c02abe31",
+ "rel": "bookmark"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-shelve/server-post-resp.xml b/doc/api_samples/os-shelve/server-post-resp.xml
new file mode 100644
index 000000000000..7b789b224689
--- /dev/null
+++ b/doc/api_samples/os-shelve/server-post-resp.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index 6f08f997ac80..ed5fdd424152 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -162,10 +162,16 @@
"compute_extension:services": "rule:admin_api",
"compute_extension:v3:os-services": "rule:admin_api",
"compute_extension:v3:servers:discoverable": "",
+ "compute_extension:shelve": "",
+ "compute_extension:shelveOffload": "rule:admin_api",
+ "compute_extension:v3:os-shelve:shelve": "",
+ "compute_extension:v3:os-shelve:shelve_offload": "rule:admin_api",
"compute_extension:simple_tenant_usage:show": "rule:admin_or_owner",
"compute_extension:v3:os-simple-tenant-usage:show": "rule:admin_or_owner",
"compute_extension:simple_tenant_usage:list": "rule:admin_api",
"compute_extension:v3:os-simple-tenant-usage:list": "rule:admin_api",
+ "compute_extension:unshelve": "",
+ "compute_extension:v3:os-shelve:unshelve": "",
"compute_extension:users": "rule:admin_api",
"compute_extension:virtual_interfaces": "",
"compute_extension:virtual_storage_arrays": "",
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 509256d87ea8..59a58103da7e 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -103,6 +103,12 @@ _STATE_MAP = {
vm_states.SOFT_DELETED: {
'default': 'DELETED',
},
+ vm_states.SHELVED: {
+ 'default': 'SHELVED',
+ },
+ vm_states.SHELVED_OFFLOADED: {
+ 'default': 'SHELVED_OFFLOADED',
+ },
}
diff --git a/nova/api/openstack/compute/contrib/shelve.py b/nova/api/openstack/compute/contrib/shelve.py
new file mode 100644
index 000000000000..778e2febad02
--- /dev/null
+++ b/nova/api/openstack/compute/contrib/shelve.py
@@ -0,0 +1,100 @@
+# Copyright 2013 Rackspace Hosting
+#
+# 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.
+
+"""The shelved mode extension."""
+
+import webob
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions as exts
+from nova.api.openstack import wsgi
+from nova import compute
+from nova import exception
+
+
+auth_shelve = exts.extension_authorizer('compute', 'shelve')
+auth_shelve_offload = exts.extension_authorizer('compute', 'shelveOffload')
+auth_unshelve = exts.extension_authorizer('compute', 'unshelve')
+
+
+class ShelveController(wsgi.Controller):
+ def __init__(self, *args, **kwargs):
+ super(ShelveController, self).__init__(*args, **kwargs)
+ self.compute_api = compute.API()
+
+ def _get_instance(self, context, instance_id):
+ try:
+ return self.compute_api.get(context, instance_id,
+ want_objects=True)
+ except exception.InstanceNotFound:
+ msg = _("Server not found")
+ raise exc.HTTPNotFound(msg)
+
+ @wsgi.action('shelve')
+ def _shelve(self, req, id, body):
+ """Move an instance into shelved mode."""
+ context = req.environ["nova.context"]
+ auth_shelve(context)
+
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.shelve(context, instance)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'shelve')
+
+ return webob.Response(status_int=202)
+
+ @wsgi.action('shelveOffload')
+ def _shelve_offload(self, req, id, body):
+ """Force removal of a shelved instance from the compute node."""
+ context = req.environ["nova.context"]
+ auth_shelve_offload(context)
+
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.shelve_offload(context, instance)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'shelveOffload')
+
+ return webob.Response(status_int=202)
+
+ @wsgi.action('unshelve')
+ def _unshelve(self, req, id, body):
+ """Restore an instance from shelved mode."""
+ context = req.environ["nova.context"]
+ auth_unshelve(context)
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.unshelve(context, instance)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'unshelve')
+ return webob.Response(status_int=202)
+
+
+class Shelve(exts.ExtensionDescriptor):
+ """Instance shelve mode."""
+
+ name = "Shelve"
+ alias = "os-shelve"
+ namespace = "http://docs.openstack.org/compute/ext/shelve/api/v1.1"
+ updated = "2013-04-06T00:00:00+00:00"
+
+ def get_controller_extensions(self):
+ controller = ShelveController()
+ extension = exts.ControllerExtension(self, 'servers', controller)
+ return [extension]
diff --git a/nova/api/openstack/compute/plugins/v3/shelve.py b/nova/api/openstack/compute/plugins/v3/shelve.py
new file mode 100644
index 000000000000..56b0683847b1
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/shelve.py
@@ -0,0 +1,105 @@
+# Copyright 2013 Rackspace Hosting
+#
+# 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.
+
+"""The shelved mode extension."""
+
+import webob
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions as exts
+from nova.api.openstack import wsgi
+from nova import compute
+from nova import exception
+
+
+ALIAS = 'os-shelve'
+auth_shelve = exts.extension_authorizer('compute', 'v3:%s:shelve' % ALIAS)
+auth_shelve_offload = exts.extension_authorizer('compute',
+ 'v3:%s:shelve_offload' % ALIAS)
+auth_unshelve = exts.extension_authorizer('compute', 'v3:%s:unshelve' % ALIAS)
+
+
+class ShelveController(wsgi.Controller):
+ def __init__(self, *args, **kwargs):
+ super(ShelveController, self).__init__(*args, **kwargs)
+ self.compute_api = compute.API()
+
+ def _get_instance(self, context, instance_id):
+ try:
+ return self.compute_api.get(context, instance_id,
+ want_objects=True)
+ except exception.InstanceNotFound:
+ msg = _("Server not found")
+ raise exc.HTTPNotFound(msg)
+
+ @wsgi.action('shelve')
+ def _shelve(self, req, id, body):
+ """Move an instance into shelved mode."""
+ context = req.environ["nova.context"]
+ auth_shelve(context)
+
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.shelve(context, instance)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'shelve')
+
+ return webob.Response(status_int=202)
+
+ @wsgi.action('shelve_offload')
+ def _shelve_offload(self, req, id, body):
+ """Force removal of a shelved instance from the compute node."""
+ context = req.environ["nova.context"]
+ auth_shelve_offload(context)
+
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.shelve_offload(context, instance)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'shelve_offload')
+
+ return webob.Response(status_int=202)
+
+ @wsgi.action('unshelve')
+ def _unshelve(self, req, id, body):
+ """Restore an instance from shelved mode."""
+ context = req.environ["nova.context"]
+ auth_unshelve(context)
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.unshelve(context, instance)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'unshelve')
+ return webob.Response(status_int=202)
+
+
+class Shelve(exts.V3APIExtensionBase):
+ """Instance shelve mode."""
+
+ name = "Shelve"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/shelve/api/v3"
+ version = 1
+
+ def get_controller_extensions(self):
+ controller = ShelveController()
+ extension = exts.ControllerExtension(self, 'servers', controller)
+ return [extension]
+
+ def get_resources(self):
+ return []
diff --git a/nova/tests/api/openstack/compute/contrib/test_shelve.py b/nova/tests/api/openstack/compute/contrib/test_shelve.py
new file mode 100644
index 000000000000..125d4f715404
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_shelve.py
@@ -0,0 +1,107 @@
+# 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.
+
+import uuid
+
+from nova.api.openstack.compute.contrib import shelve
+from nova import db
+from nova import exception
+from nova.openstack.common import policy
+from nova import test
+from nova.tests.api.openstack import fakes
+from nova.tests import fake_instance
+
+
+class ShelvePolicyTest(test.TestCase):
+ def setUp(self):
+ super(ShelvePolicyTest, self).setUp()
+ self.controller = shelve.ShelveController()
+
+ def test_shelve_restricted_by_role(self):
+ rules = policy.Rules({'compute_extension:shelve':
+ policy.parse_rule('role:admin')})
+ policy.set_rules(rules)
+
+ req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve')
+ self.assertRaises(exception.NotAuthorized, self.controller._shelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_shelve_allowed(self):
+ rules = policy.Rules({'compute:get': policy.parse_rule(''),
+ 'compute_extension:shelve':
+ policy.parse_rule('')})
+ policy.set_rules(rules)
+
+ def fake_instance_get_by_uuid(context, instance_id,
+ columns_to_join=None):
+ return fake_instance.fake_db_instance(
+ **{'name': 'fake', 'project_id': '%s_unequal' %
+ context.project_id})
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve')
+ self.assertRaises(exception.NotAuthorized, self.controller._shelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_unshelve_restricted_by_role(self):
+ rules = policy.Rules({'compute_extension:unshelve':
+ policy.parse_rule('role:admin')})
+ policy.set_rules(rules)
+
+ req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve')
+ self.assertRaises(exception.NotAuthorized, self.controller._unshelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_unshelve_allowed(self):
+ rules = policy.Rules({'compute:get': policy.parse_rule(''),
+ 'compute_extension:unshelve':
+ policy.parse_rule('')})
+ policy.set_rules(rules)
+
+ def fake_instance_get_by_uuid(context, instance_id,
+ columns_to_join=None):
+ return fake_instance.fake_db_instance(
+ **{'name': 'fake', 'project_id': '%s_unequal' %
+ context.project_id})
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve')
+ self.assertRaises(exception.NotAuthorized, self.controller._unshelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_shelve_offload_restricted_by_role(self):
+ rules = policy.Rules({'compute_extension:shelveOffload':
+ policy.parse_rule('role:admin')})
+ policy.set_rules(rules)
+
+ req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve')
+ self.assertRaises(exception.NotAuthorized,
+ self.controller._shelve_offload, req, str(uuid.uuid4()), {})
+
+ def test_shelve_offload_allowed(self):
+ rules = policy.Rules({'compute:get': policy.parse_rule(''),
+ 'compute_extension:shelveOffload':
+ policy.parse_rule('')})
+ policy.set_rules(rules)
+
+ def fake_instance_get_by_uuid(context, instance_id,
+ columns_to_join=None):
+ return fake_instance.fake_db_instance(
+ **{'name': 'fake', 'project_id': '%s_unequal' %
+ context.project_id})
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve')
+ self.assertRaises(exception.NotAuthorized,
+ self.controller._shelve_offload, req, str(uuid.uuid4()), {})
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index fb06ee35f3d5..691b031e0daf 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -238,10 +238,16 @@ policy_data = """
"compute_extension:server_usage": "",
"compute_extension:services": "",
"compute_extension:v3:os-services": "",
+ "compute_extension:shelve": "",
+ "compute_extension:shelveOffload": "",
+ "compute_extension:v3:os-shelve:shelve": "",
+ "compute_extension:v3:os-shelve:shelve_offload": "",
"compute_extension:simple_tenant_usage:show": "",
"compute_extension:v3:os-simple-tenant-usage:show": "",
"compute_extension:simple_tenant_usage:list": "",
"compute_extension:v3:os-simple-tenant-usage:list": "",
+ "compute_extension:unshelve": "",
+ "compute_extension:v3:os-shelve:unshelve": "",
"compute_extension:users": "",
"compute_extension:virtual_interfaces": "",
"compute_extension:virtual_storage_arrays": "",
diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
index 23ce1f611cec..eca786e27470 100644
--- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
+++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
@@ -512,6 +512,14 @@
"namespace": "http://docs.openstack.org/compute/ext/servers/api/v1.1",
"updated": "%(timestamp)s"
},
+ {
+ "alias": "os-shelve",
+ "description": "%(text)s",
+ "links": [],
+ "name": "Shelve",
+ "namespace": "http://docs.openstack.org/compute/ext/shelve/api/v1.1",
+ "updated": "%(timestamp)s"
+ },
{
"alias": "os-simple-tenant-usage",
"description": "%(text)s",
diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
index 31a755ec2e2b..8edf7f7e3d80 100644
--- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
+++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
@@ -192,6 +192,9 @@
%(text)s
+
+ %(text)s
+
%(text)s
diff --git a/nova/tests/integrated/api_samples/os-shelve/os-shelve.json.tpl b/nova/tests/integrated/api_samples/os-shelve/os-shelve.json.tpl
new file mode 100644
index 000000000000..5a19f85cffa9
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-shelve/os-shelve.json.tpl
@@ -0,0 +1,3 @@
+{
+ "%(action)s": null
+}
diff --git a/nova/tests/integrated/api_samples/os-shelve/os-shelve.xml.tpl b/nova/tests/integrated/api_samples/os-shelve/os-shelve.xml.tpl
new file mode 100644
index 000000000000..41d18bdac00e
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-shelve/os-shelve.xml.tpl
@@ -0,0 +1,2 @@
+
+ <%(action)s/>
diff --git a/nova/tests/integrated/api_samples/os-shelve/server-post-req.json.tpl b/nova/tests/integrated/api_samples/os-shelve/server-post-req.json.tpl
new file mode 100644
index 000000000000..d3916d1aa68a
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-shelve/server-post-req.json.tpl
@@ -0,0 +1,16 @@
+{
+ "server" : {
+ "name" : "new-server-test",
+ "imageRef" : "%(host)s/openstack/images/%(image_id)s",
+ "flavorRef" : "%(host)s/openstack/flavors/1",
+ "metadata" : {
+ "My Server Name" : "Apache1"
+ },
+ "personality" : [
+ {
+ "path" : "/etc/banner.txt",
+ "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
+ }
+ ]
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-shelve/server-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-shelve/server-post-req.xml.tpl
new file mode 100644
index 000000000000..f92614984242
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-shelve/server-post-req.xml.tpl
@@ -0,0 +1,19 @@
+
+
+
+ Apache1
+
+
+
+ ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
+ dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
+ IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
+ c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
+ QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
+ ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
+ dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
+ c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
+ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
+
+
+
diff --git a/nova/tests/integrated/api_samples/os-shelve/server-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-shelve/server-post-resp.json.tpl
new file mode 100644
index 000000000000..d5f030c8730b
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-shelve/server-post-resp.json.tpl
@@ -0,0 +1,16 @@
+{
+ "server": {
+ "adminPass": "%(password)s",
+ "id": "%(id)s",
+ "links": [
+ {
+ "href": "%(host)s/v2/openstack/servers/%(uuid)s",
+ "rel": "self"
+ },
+ {
+ "href": "%(host)s/openstack/servers/%(uuid)s",
+ "rel": "bookmark"
+ }
+ ]
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-shelve/server-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-shelve/server-post-resp.xml.tpl
new file mode 100644
index 000000000000..3bb13e69bd6d
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-shelve/server-post-resp.xml.tpl
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py
index d8ce7c9eedb3..c68630134510 100644
--- a/nova/tests/integrated/test_api_samples.py
+++ b/nova/tests/integrated/test_api_samples.py
@@ -1522,6 +1522,40 @@ class RescueXmlTest(RescueJsonTest):
ctype = 'xml'
+class ShelveJsonTest(ServersSampleBase):
+ extension_name = "nova.api.openstack.compute.contrib.shelve.Shelve"
+
+ def setUp(self):
+ super(ShelveJsonTest, self).setUp()
+ # Don't offload instance, so we can test the offload call.
+ CONF.shelved_offload_time = -1
+
+ def _test_server_action(self, uuid, action):
+ response = self._do_post('servers/%s/action' % uuid,
+ 'os-shelve',
+ {'action': action})
+ self.assertEqual(response.status, 202)
+ self.assertEqual(response.read(), "")
+
+ def test_shelve(self):
+ uuid = self._post_server()
+ self._test_server_action(uuid, 'shelve')
+
+ def test_shelve_offload(self):
+ uuid = self._post_server()
+ self._test_server_action(uuid, 'shelve')
+ self._test_server_action(uuid, 'shelveOffload')
+
+ def test_unshelve(self):
+ uuid = self._post_server()
+ self._test_server_action(uuid, 'shelve')
+ self._test_server_action(uuid, 'unshelve')
+
+
+class ShelveXmlTest(ShelveJsonTest):
+ ctype = 'xml'
+
+
class VirtualInterfacesJsonTest(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib"
".virtual_interfaces.Virtual_interfaces")