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'