diff options
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/rescue.py | 96 | ||||
-rw-r--r-- | nova/tests/api/openstack/compute/plugins/v3/test_rescue.py | 130 |
2 files changed, 226 insertions, 0 deletions
diff --git a/nova/api/openstack/compute/plugins/v3/rescue.py b/nova/api/openstack/compute/plugins/v3/rescue.py new file mode 100644 index 000000000..c89d11117 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/rescue.py @@ -0,0 +1,96 @@ +# Copyright 2011 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. + +"""The rescue mode extension.""" + +from oslo.config import cfg +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 +from nova import utils + + +CONF = cfg.CONF +authorize = exts.extension_authorizer('compute', 'rescue') + + +class RescueController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(RescueController, 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) + except exception.InstanceNotFound: + msg = _("Server not found") + raise exc.HTTPNotFound(msg) + + @wsgi.action('rescue') + def _rescue(self, req, id, body): + """Rescue an instance.""" + context = req.environ["nova.context"] + authorize(context) + + if body['rescue'] and 'adminPass' in body['rescue']: + password = body['rescue']['adminPass'] + else: + password = utils.generate_password() + + instance = self._get_instance(context, id) + try: + self.compute_api.rescue(context, instance, + rescue_password=password) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'rescue') + except exception.InvalidVolume as volume_error: + raise exc.HTTPConflict(explanation=volume_error.format_message()) + except exception.InstanceNotRescuable as non_rescuable: + raise exc.HTTPBadRequest( + explanation=non_rescuable.format_message()) + + return {'adminPass': password} + + @wsgi.action('unrescue') + def _unrescue(self, req, id, body): + """Unrescue an instance.""" + context = req.environ["nova.context"] + authorize(context) + instance = self._get_instance(context, id) + try: + self.compute_api.unrescue(context, instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'unrescue') + return webob.Response(status_int=202) + + +class Rescue(exts.ExtensionDescriptor): + """Instance rescue mode.""" + + name = "Rescue" + alias = "os-rescue" + namespace = "http://docs.openstack.org/compute/ext/rescue/api/v1.1" + updated = "2011-08-18T00:00:00+00:00" + + def get_controller_extensions(self): + controller = RescueController() + extension = exts.ControllerExtension(self, 'servers', controller) + return [extension] diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_rescue.py b/nova/tests/api/openstack/compute/plugins/v3/test_rescue.py new file mode 100644 index 000000000..ea0a96cbf --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_rescue.py @@ -0,0 +1,130 @@ +# Copyright 2011 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 oslo.config import cfg +import webob + +from nova import compute +from nova import exception +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 rescue(self, context, instance, rescue_password=None): + pass + + +def unrescue(self, context, instance): + pass + + +class RescueTest(test.TestCase): + def setUp(self): + super(RescueTest, self).setUp() + + def fake_compute_get(*args, **kwargs): + uuid = '70f6db34-de8d-4fbd-aafb-4065bdfa6114' + return {'id': 1, 'uuid': uuid} + + self.stubs.Set(compute.api.API, "get", fake_compute_get) + self.stubs.Set(compute.api.API, "rescue", rescue) + self.stubs.Set(compute.api.API, "unrescue", unrescue) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Rescue']) + self.app = fakes.wsgi_app(init_only=('servers',)) + + def test_rescue_with_preset_password(self): + body = {"rescue": {"adminPass": "AABBCC112233"}} + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + resp_json = jsonutils.loads(resp.body) + self.assertEqual("AABBCC112233", resp_json['adminPass']) + + def test_rescue_generates_password(self): + body = dict(rescue=None) + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + resp_json = jsonutils.loads(resp.body) + self.assertEqual(CONF.password_length, len(resp_json['adminPass'])) + + def test_rescue_of_rescued_instance(self): + body = dict(rescue=None) + + def fake_rescue(*args, **kwargs): + raise exception.InstanceInvalidState('fake message') + + self.stubs.Set(compute.api.API, "rescue", fake_rescue) + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 409) + + def test_unrescue(self): + body = dict(unrescue=None) + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 202) + + def test_unrescue_of_active_instance(self): + body = dict(unrescue=None) + + def fake_unrescue(*args, **kwargs): + raise exception.InstanceInvalidState('fake message') + + self.stubs.Set(compute.api.API, "unrescue", fake_unrescue) + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 409) + + def test_rescue_raises_unrescuable(self): + body = dict(rescue=None) + + def fake_rescue(*args, **kwargs): + raise exception.InstanceNotRescuable('fake message') + + self.stubs.Set(compute.api.API, "rescue", fake_rescue) + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 400) |