diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json
index 3cb31fc2d7a9..439d1af18fc3 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.json
+++ b/doc/api_samples/all_extensions/extensions-get-resp.json
@@ -184,6 +184,14 @@
"namespace": "http://docs.openstack.org/compute/ext/deferred-delete/api/v1.1",
"updated": "2011-09-01T00:00:00+00:00"
},
+ {
+ "alias": "os-evacuate",
+ "description": "Enables server evacuation",
+ "links": [],
+ "name": "Evacuate",
+ "namespace": "http://docs.openstack.org/compute/ext/evacuate/api/v2",
+ "updated": "2012-12-05T00:00:00+00:00"
+ },
{
"alias": "os-fixed-ips",
"description": "Fixed IPs support.",
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml
index 8b0b218aa527..71f79dc3a8b5 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.xml
+++ b/doc/api_samples/all_extensions/extensions-get-resp.xml
@@ -84,6 +84,9 @@
Instance deferred delete.
+
+ Enables server evacuation
+
Fixed IPs support.
diff --git a/doc/api_samples/os-evacuate/server-evacuate-req.json b/doc/api_samples/os-evacuate/server-evacuate-req.json
new file mode 100644
index 000000000000..72a90e19a5ff
--- /dev/null
+++ b/doc/api_samples/os-evacuate/server-evacuate-req.json
@@ -0,0 +1,7 @@
+{
+ "evacuate": {
+ "host": "TargetHost",
+ "adminPass": "MySecretPass",
+ "onSharedStorage": "True"
+ }
+}
diff --git a/doc/api_samples/os-evacuate/server-evacuate-req.xml b/doc/api_samples/os-evacuate/server-evacuate-req.xml
new file mode 100644
index 000000000000..636772dcaff1
--- /dev/null
+++ b/doc/api_samples/os-evacuate/server-evacuate-req.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/doc/api_samples/os-evacuate/server-evacuate-resp.json b/doc/api_samples/os-evacuate/server-evacuate-resp.json
new file mode 100644
index 000000000000..a023c720b974
--- /dev/null
+++ b/doc/api_samples/os-evacuate/server-evacuate-resp.json
@@ -0,0 +1,3 @@
+{
+ "adminPass": "MySecretPass"
+}
diff --git a/doc/api_samples/os-evacuate/server-evacuate-resp.xml b/doc/api_samples/os-evacuate/server-evacuate-resp.xml
new file mode 100644
index 000000000000..582388670248
--- /dev/null
+++ b/doc/api_samples/os-evacuate/server-evacuate-resp.xml
@@ -0,0 +1,2 @@
+
+MySecretPass
\ No newline at end of file
diff --git a/doc/api_samples/os-evacuate/server-post-req.json b/doc/api_samples/os-evacuate/server-post-req.json
new file mode 100644
index 000000000000..d88eb4122223
--- /dev/null
+++ b/doc/api_samples/os-evacuate/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-evacuate/server-post-req.xml b/doc/api_samples/os-evacuate/server-post-req.xml
new file mode 100644
index 000000000000..0a3c8bb5303d
--- /dev/null
+++ b/doc/api_samples/os-evacuate/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-evacuate/server-post-resp.json b/doc/api_samples/os-evacuate/server-post-resp.json
new file mode 100644
index 000000000000..d9114225a2bf
--- /dev/null
+++ b/doc/api_samples/os-evacuate/server-post-resp.json
@@ -0,0 +1,16 @@
+{
+ "server": {
+ "adminPass": "xjDVAYHmc34s",
+ "id": "784f5005-bec9-4c22-8c42-5a7dcba88d82",
+ "links": [
+ {
+ "href": "http://openstack.example.com/v2/openstack/servers/784f5005-bec9-4c22-8c42-5a7dcba88d82",
+ "rel": "self"
+ },
+ {
+ "href": "http://openstack.example.com/openstack/servers/784f5005-bec9-4c22-8c42-5a7dcba88d82",
+ "rel": "bookmark"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-evacuate/server-post-resp.xml b/doc/api_samples/os-evacuate/server-post-resp.xml
new file mode 100644
index 000000000000..3a31871ba9af
--- /dev/null
+++ b/doc/api_samples/os-evacuate/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 7dc7d20f0750..923c4a5285aa 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -40,6 +40,7 @@
"compute_extension:createserverext": "",
"compute_extension:deferred_delete": "",
"compute_extension:disk_config": "",
+ "compute_extension:evacuate": "rule:admin_api",
"compute_extension:extended_server_attributes": "rule:admin_api",
"compute_extension:extended_status": "",
"compute_extension:extended_availability_zone": "",
diff --git a/nova/api/openstack/compute/contrib/evacuate.py b/nova/api/openstack/compute/contrib/evacuate.py
new file mode 100644
index 000000000000..4c9229d1e1a2
--- /dev/null
+++ b/nova/api/openstack/compute/contrib/evacuate.py
@@ -0,0 +1,98 @@
+# Copyright 2013 OpenStack, LLC.
+#
+# 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 webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova import compute
+from nova import exception
+from nova.openstack.common import log as logging
+from nova import utils
+
+LOG = logging.getLogger(__name__)
+authorize = extensions.extension_authorizer('compute', 'evacuate')
+
+
+class Controller(wsgi.Controller):
+ def __init__(self, *args, **kwargs):
+ super(Controller, self).__init__(*args, **kwargs)
+ self.compute_api = compute.API()
+
+ @wsgi.action('evacuate')
+ def _evacuate(self, req, id, body):
+ """
+ Permit admins to evacuate a server from a failed host
+ to a new one.
+ """
+ context = req.environ["nova.context"]
+ if not context.is_admin:
+ msg = _("Instance evacuate is admin only functionality")
+ raise exc.HTTPForbidden(explanation=msg)
+ authorize(context)
+
+ try:
+ if len(body) != 1:
+ raise exc.HTTPBadRequest(_("Malformed request body"))
+
+ evacuate_body = body["evacuate"]
+ host = evacuate_body["host"]
+ on_shared_storage = utils.bool_from_str(
+ evacuate_body["onSharedStorage"])
+
+ if 'adminPass' in evacuate_body:
+ # check that if requested to evacuate server on shared storage
+ # password not specified
+ if on_shared_storage:
+ msg = _("admin password can't be changed on existing disk")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ password = evacuate_body['adminPass']
+ elif not on_shared_storage:
+ password = utils.generate_password()
+
+ except (TypeError, KeyError):
+ msg = _("host and onSharedStorage must be specified.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ instance = self.compute_api.get(context, id)
+ self.compute_api.evacuate(context, instance, host,
+ on_shared_storage, password)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'evacuate')
+ except Exception as e:
+ msg = _("Error in evacuate, %s") % e
+ LOG.exception(msg, instance=instance)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ if password:
+ return {'adminPass': password}
+
+
+class Evacuate(extensions.ExtensionDescriptor):
+ """Enables server evacuation."""
+
+ name = "Evacuate"
+ alias = "os-evacuate"
+ namespace = "http://docs.openstack.org/compute/ext/evacuate/api/v2"
+ updated = "2013-01-06T00:00:00+00:00"
+
+ def get_controller_extensions(self):
+ controller = Controller()
+ extension = extensions.ControllerExtension(self, 'servers', controller)
+ return [extension]
diff --git a/nova/tests/api/openstack/compute/contrib/test_evacuate.py b/nova/tests/api/openstack/compute/contrib/test_evacuate.py
new file mode 100644
index 000000000000..f76bf7bcfd1f
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_evacuate.py
@@ -0,0 +1,156 @@
+# Copyright 2013 OpenStack LLC.
+#
+# 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
+
+import webob
+
+from nova.compute import api as compute_api
+from nova.compute import vm_states
+from nova import context
+from nova.openstack.common import cfg
+from nova.openstack.common import jsonutils
+from nova import test
+from nova.tests.api.openstack import fakes
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+
+
+def fake_compute_api(*args, **kwargs):
+ return True
+
+
+def fake_compute_api_get(self, context, instance_id):
+ return {
+ 'id': 1,
+ 'uuid': instance_id,
+ 'vm_state': vm_states.ACTIVE,
+ 'task_state': None, 'host': 'host1'
+ }
+
+
+class EvacuateTest(test.TestCase):
+
+ _methods = ('resize', 'evacuate')
+
+ def setUp(self):
+ super(EvacuateTest, self).setUp()
+ self.stubs.Set(compute_api.API, 'get', fake_compute_api_get)
+ self.UUID = uuid.uuid4()
+ for _method in self._methods:
+ self.stubs.Set(compute_api.API, _method, fake_compute_api)
+
+ def test_evacuate_instance_with_no_target(self):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = 'fake'
+ ctxt.project_id = 'fake'
+ ctxt.is_admin = True
+ app = fakes.wsgi_app(fake_auth_context=ctxt)
+ req = webob.Request.blank('/v2/fake/servers/%s/action' % self.UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps({
+ 'evacuate': {
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'
+ }
+ })
+ req.content_type = 'application/json'
+ res = req.get_response(app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_evacuate_instance_with_target(self):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = 'fake'
+ ctxt.project_id = 'fake'
+ ctxt.is_admin = True
+ app = fakes.wsgi_app(fake_auth_context=ctxt)
+ uuid = self.UUID
+ req = webob.Request.blank('/v2/fake/servers/%s/action' % uuid)
+ req.method = 'POST'
+ req.body = jsonutils.dumps({
+ 'evacuate': {
+ 'host': 'my_host',
+ 'onSharedStorage': 'false',
+ 'adminPass': 'MyNewPass'
+ }
+ })
+ req.content_type = 'application/json'
+
+ def fake_update(inst, context, instance,
+ task_state, expected_task_state):
+ return None
+
+ self.stubs.Set(compute_api.API, 'update', fake_update)
+
+ resp = req.get_response(app)
+ self.assertEqual(resp.status_int, 200)
+ resp_json = jsonutils.loads(resp.body)
+ self.assertEqual("MyNewPass", resp_json['adminPass'])
+
+ def test_evacuate_shared_and_pass(self):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = 'fake'
+ ctxt.project_id = 'fake'
+ ctxt.is_admin = True
+ app = fakes.wsgi_app(fake_auth_context=ctxt)
+ uuid = self.UUID
+ req = webob.Request.blank('/v2/fake/servers/%s/action' % uuid)
+ req.method = 'POST'
+ req.body = jsonutils.dumps({
+ 'evacuate': {
+ 'host': 'my_host',
+ 'onSharedStorage': 'True',
+ 'adminPass': 'MyNewPass'
+ }
+ })
+ req.content_type = 'application/json'
+
+ def fake_update(inst, context, instance,
+ task_state, expected_task_state):
+ return None
+
+ self.stubs.Set(compute_api.API, 'update', fake_update)
+
+ res = req.get_response(app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_evacuate_not_shared_pass_generated(self):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = 'fake'
+ ctxt.project_id = 'fake'
+ ctxt.is_admin = True
+ app = fakes.wsgi_app(fake_auth_context=ctxt)
+ uuid = self.UUID
+ req = webob.Request.blank('/v2/fake/servers/%s/action' % uuid)
+ req.method = 'POST'
+ req.body = jsonutils.dumps({
+ 'evacuate': {
+ 'host': 'my_host',
+ 'onSharedStorage': 'False',
+ }
+ })
+
+ req.content_type = 'application/json'
+
+ def fake_update(inst, context, instance,
+ task_state, expected_task_state):
+ return None
+
+ self.stubs.Set(compute_api.API, 'update', fake_update)
+
+ resp = req.get_response(app)
+ self.assertEqual(resp.status_int, 200)
+ resp_json = jsonutils.loads(resp.body)
+ self.assertEqual(CONF.password_length, len(resp_json['adminPass']))
diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py
index 52798b9aade7..a52b0e0fc3ae 100644
--- a/nova/tests/api/openstack/compute/test_extensions.py
+++ b/nova/tests/api/openstack/compute/test_extensions.py
@@ -167,6 +167,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"DeferredDelete",
"DiskConfig",
"ExtendedAvailabilityZone",
+ "Evacuate",
"ExtendedStatus",
"ExtendedServerAttributes",
"FixedIPs",
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index 43d9e72b360d..4b4c41e54a97 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -117,6 +117,7 @@ policy_data = """
"compute_extension:createserverext": "",
"compute_extension:deferred_delete": "",
"compute_extension:disk_config": "",
+ "compute_extension:evacuate": "",
"compute_extension:extended_server_attributes": "",
"compute_extension:extended_status": "",
"compute_extension:extended_availability_zone": "",
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 50415fef3838..35d50d0254ad 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
@@ -184,6 +184,14 @@
"namespace": "http://docs.openstack.org/compute/ext/deferred-delete/api/v1.1",
"updated": "%(timestamp)s"
},
+ {
+ "alias": "os-evacuate",
+ "description": "%(text)s",
+ "links": [],
+ "name": "Evacuate",
+ "namespace": "http://docs.openstack.org/compute/ext/evacuate/api/v2",
+ "updated": "%(timestamp)s"
+ },
{
"alias": "os-fixed-ips",
"description": "Fixed IPs support.",
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 f7251d13be01..2adc5988c269 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
@@ -69,6 +69,9 @@
%(text)s
+
+ %(text)s
+
Fixed IPs support.
diff --git a/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-req.json.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-req.json.tpl
new file mode 100644
index 000000000000..179cddce73b1
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-req.json.tpl
@@ -0,0 +1,7 @@
+{
+ "evacuate": {
+ "host": "%(host)s",
+ "adminPass": "%(adminPass)s",
+ "onSharedStorage": "%(onSharedStorage)s"
+ }
+}
diff --git a/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-req.xml.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-req.xml.tpl
new file mode 100644
index 000000000000..b0471f916229
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-req.xml.tpl
@@ -0,0 +1,5 @@
+
+
diff --git a/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-resp.json.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-resp.json.tpl
new file mode 100644
index 000000000000..0da07da5b8f0
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-resp.json.tpl
@@ -0,0 +1,3 @@
+{
+ "adminPass": "%(password)s"
+}
diff --git a/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-resp.xml.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-resp.xml.tpl
new file mode 100644
index 000000000000..2a779af6d1d3
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/server-evacuate-resp.xml.tpl
@@ -0,0 +1 @@
+%(password)s
\ No newline at end of file
diff --git a/nova/tests/integrated/api_samples/os-evacuate/server-post-req.json.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-post-req.json.tpl
new file mode 100644
index 000000000000..d3916d1aa68a
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/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-evacuate/server-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-post-req.xml.tpl
new file mode 100644
index 000000000000..f92614984242
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/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-evacuate/server-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-post-resp.json.tpl
new file mode 100644
index 000000000000..d5f030c8730b
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/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-evacuate/server-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-evacuate/server-post-resp.xml.tpl
new file mode 100644
index 000000000000..3bb13e69bd6d
--- /dev/null
+++ b/nova/tests/integrated/api_samples/os-evacuate/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 d688d900df73..a7c216db1837 100644
--- a/nova/tests/integrated/test_api_samples.py
+++ b/nova/tests/integrated/test_api_samples.py
@@ -42,6 +42,7 @@ from nova.openstack.common.log import logging
from nova.openstack.common import timeutils
import nova.quota
from nova.scheduler import driver
+from nova.servicegroup import api as service_group_api
from nova import test
from nova.tests.api.openstack.compute.contrib import test_fping
from nova.tests.api.openstack.compute.contrib import test_networks
@@ -2881,3 +2882,36 @@ class ExtendedAvailabilityZoneJsonTests(ServersSampleBase):
class ExtendedAvailabilityZoneXmlTests(ExtendedAvailabilityZoneJsonTests):
ctype = 'xml'
+
+
+class EvacuateJsonTest(ServersSampleBase):
+
+ extension_name = ("nova.api.openstack.compute.contrib"
+ ".evacuate.Evacuate")
+
+ def test_server_evacuate(self):
+ uuid = self._post_server()
+
+ req_subs = {
+ 'host': 'TargetHost',
+ "adminPass": "MySecretPass",
+ "onSharedStorage": 'False'
+ }
+
+ def fake_service_is_up(self, service):
+ """Simulate validation of instance host is down."""
+ return False
+
+ self.stubs.Set(service_group_api.API, 'service_is_up',
+ fake_service_is_up)
+
+ response = self._do_post('servers/%s/action' % uuid,
+ 'server-evacuate-req', req_subs)
+ self.assertEqual(response.status, 200)
+ subs = self._get_regexes()
+ return self._verify_response('server-evacuate-resp', subs,
+ response)
+
+
+class EvacuateXmlTest(EvacuateJsonTest):
+ ctype = 'xml'