diff options
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/evacuate.py | 97 | ||||
-rw-r--r-- | nova/tests/api/openstack/compute/plugins/v3/test_evacuate.py | 198 |
2 files changed, 295 insertions, 0 deletions
diff --git a/nova/api/openstack/compute/plugins/v3/evacuate.py b/nova/api/openstack/compute/plugins/v3/evacuate.py new file mode 100644 index 000000000..7eee99ed1 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/evacuate.py @@ -0,0 +1,97 @@ +# Copyright 2013 OpenStack Foundation +# +# 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.openstack.common import strutils +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"] + authorize(context) + + try: + if len(body) != 1: + raise exc.HTTPBadRequest(_("Malformed request body")) + + evacuate_body = body["evacuate"] + host = evacuate_body["host"] + on_shared_storage = strutils.bool_from_string( + evacuate_body["onSharedStorage"]) + + password = None + 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/plugins/v3/test_evacuate.py b/nova/tests/api/openstack/compute/plugins/v3/test_evacuate.py new file mode 100644 index 000000000..816bac565 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_evacuate.py @@ -0,0 +1,198 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo.config import cfg +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 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'])) + + def test_evacuate_shared(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', + } + }) + 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, 200) + + def test_not_admin(self): + ctxt = context.RequestContext('fake', 'fake', is_admin=False) + 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', + } + }) + req.content_type = 'application/json' + res = req.get_response(app) + self.assertEqual(res.status_int, 403) |