diff --git a/doc/v3/api_samples/os-suspend-server/server-post-req.json b/doc/v3/api_samples/os-suspend-server/server-post-req.json new file mode 100644 index 0000000000..2eedab6147 --- /dev/null +++ b/doc/v3/api_samples/os-suspend-server/server-post-req.json @@ -0,0 +1,10 @@ +{ + "server" : { + "name" : "new-server-test", + "image_ref" : "http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "flavor_ref" : "http://openstack.example.com/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + } + } +} diff --git a/doc/v3/api_samples/os-suspend-server/server-post-resp.json b/doc/v3/api_samples/os-suspend-server/server-post-resp.json new file mode 100644 index 0000000000..270cb84634 --- /dev/null +++ b/doc/v3/api_samples/os-suspend-server/server-post-resp.json @@ -0,0 +1,16 @@ +{ + "server": { + "admin_password": "DM3QzjhGTzLB", + "id": "bebeec79-497e-4711-a311-d0d2e3dfc73b", + "links": [ + { + "href": "http://openstack.example.com/v3/servers/bebeec79-497e-4711-a311-d0d2e3dfc73b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/servers/bebeec79-497e-4711-a311-d0d2e3dfc73b", + "rel": "bookmark" + } + ] + } +} diff --git a/doc/v3/api_samples/os-admin-actions/admin-actions-resume.json b/doc/v3/api_samples/os-suspend-server/server-resume.json similarity index 100% rename from doc/v3/api_samples/os-admin-actions/admin-actions-resume.json rename to doc/v3/api_samples/os-suspend-server/server-resume.json diff --git a/doc/v3/api_samples/os-admin-actions/admin-actions-suspend.json b/doc/v3/api_samples/os-suspend-server/server-suspend.json similarity index 100% rename from doc/v3/api_samples/os-admin-actions/admin-actions-suspend.json rename to doc/v3/api_samples/os-suspend-server/server-suspend.json diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 0d24e1d360..8c19844d40 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -39,8 +39,6 @@ "compute_extension:admin_actions:migrate": "rule:admin_api", "compute_extension:v3:os-admin-actions": "rule:admin_api", "compute_extension:v3:os-admin-actions:discoverable": "", - "compute_extension:v3:os-admin-actions:suspend": "rule:admin_or_owner", - "compute_extension:v3:os-admin-actions:resume": "rule:admin_or_owner", "compute_extension:v3:os-admin-actions:reset_network": "rule:admin_api", "compute_extension:v3:os-admin-actions:inject_network_info": "rule:admin_api", "compute_extension:v3:os-admin-actions:create_backup": "rule:admin_or_owner", @@ -226,6 +224,9 @@ "compute_extension:v3:os-shelve:shelve:discoverable": "", "compute_extension:v3:os-shelve:shelve_offload": "rule:admin_api", "compute_extension:simple_tenant_usage:show": "rule:admin_or_owner", + "compute_extension:v3:os-suspend-server:discoverable": "", + "compute_extension:v3:os-suspend-server:suspend": "rule:admin_or_owner", + "compute_extension:v3:os-suspend-server:resume": "rule:admin_or_owner", "compute_extension:v3:os-simple-tenant-usage:show": "rule:admin_or_owner", "compute_extension:v3:os-simple-tenant-usage:discoverable": "", "compute_extension:simple_tenant_usage:list": "rule:admin_api", diff --git a/nova/api/openstack/compute/plugins/v3/admin_actions.py b/nova/api/openstack/compute/plugins/v3/admin_actions.py index 2a5e2a5489..dc96dd9dd3 100644 --- a/nova/api/openstack/compute/plugins/v3/admin_actions.py +++ b/nova/api/openstack/compute/plugins/v3/admin_actions.py @@ -44,42 +44,6 @@ class AdminActionsController(wsgi.Controller): super(AdminActionsController, self).__init__(*args, **kwargs) self.compute_api = compute.API() - @extensions.expected_errors((404, 409)) - @wsgi.action('suspend') - def _suspend(self, req, id, body): - """Permit admins to suspend the server.""" - context = req.environ['nova.context'] - authorize(context, 'suspend') - try: - server = self.compute_api.get(context, id, want_objects=True) - self.compute_api.suspend(context, server) - except exception.InstanceIsLocked as e: - raise exc.HTTPConflict(explanation=e.format_message()) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'suspend') - except exception.InstanceNotFound as e: - raise exc.HTTPNotFound(explanation=e.format_message()) - return webob.Response(status_int=202) - - @extensions.expected_errors((404, 409)) - @wsgi.action('resume') - def _resume(self, req, id, body): - """Permit admins to resume the server from suspend.""" - context = req.environ['nova.context'] - authorize(context, 'resume') - try: - server = self.compute_api.get(context, id, want_objects=True) - self.compute_api.resume(context, server) - except exception.InstanceIsLocked as e: - raise exc.HTTPConflict(explanation=e.format_message()) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'resume') - except exception.InstanceNotFound as e: - raise exc.HTTPNotFound(explanation=e.format_message()) - return webob.Response(status_int=202) - @extensions.expected_errors((400, 404, 409, 413)) @wsgi.action('migrate') def _migrate(self, req, id, body): diff --git a/nova/api/openstack/compute/plugins/v3/suspend_server.py b/nova/api/openstack/compute/plugins/v3/suspend_server.py new file mode 100644 index 0000000000..026a8ba25f --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/suspend_server.py @@ -0,0 +1,90 @@ +# Copyright 2013 IBM Corp. +# +# 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 webob +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 + +LOG = logging.getLogger(__name__) +ALIAS = "os-suspend-server" + + +def authorize(context, action_name): + action = 'v3:%s:%s' % (ALIAS, action_name) + extensions.extension_authorizer('compute', action)(context) + + +class SuspendServerController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(SuspendServerController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + @extensions.expected_errors((404, 409)) + @wsgi.action('suspend') + def _suspend(self, req, id, body): + """Permit admins to suspend the server.""" + context = req.environ['nova.context'] + authorize(context, 'suspend') + try: + server = self.compute_api.get(context, id, want_objects=True) + self.compute_api.suspend(context, server) + except exception.InstanceIsLocked as e: + raise exc.HTTPConflict(explanation=e.format_message()) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'suspend') + except exception.InstanceNotFound as e: + raise exc.HTTPNotFound(explanation=e.format_message()) + return webob.Response(status_int=202) + + @extensions.expected_errors((404, 409)) + @wsgi.action('resume') + def _resume(self, req, id, body): + """Permit admins to resume the server from suspend.""" + context = req.environ['nova.context'] + authorize(context, 'resume') + try: + server = self.compute_api.get(context, id, want_objects=True) + self.compute_api.resume(context, server) + except exception.InstanceIsLocked as e: + raise exc.HTTPConflict(explanation=e.format_message()) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'resume') + except exception.InstanceNotFound as e: + raise exc.HTTPNotFound(explanation=e.format_message()) + return webob.Response(status_int=202) + + +class SuspendServer(extensions.V3APIExtensionBase): + """Enable suspend/resume server actions.""" + + name = "SuspendServer" + alias = ALIAS + namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS + version = 1 + + def get_controller_extensions(self): + controller = SuspendServerController() + extension = extensions.ControllerExtension(self, 'servers', controller) + return [extension] + + def get_resources(self): + return [] diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py b/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py index 4be16d4c3a..e68f11b856 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py +++ b/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py @@ -156,8 +156,7 @@ class CommonMixin(object): class AdminActionsTest(CommonMixin, test.NoDBTestCase): def test_actions(self): - actions = ['suspend', 'resume', 'migrate', - 'reset_network', 'inject_network_info'] + actions = ['migrate', 'reset_network', 'inject_network_info'] method_translations = {'migrate': 'resize'} for action in actions: @@ -168,7 +167,7 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_raise_conflict_on_invalid_state(self): - actions = ['suspend', 'resume', 'migrate', 'migrate_live'] + actions = ['migrate', 'migrate_live'] method_translations = {'migrate': 'resize', 'migrate_live': 'live_migrate'} @@ -187,8 +186,7 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_with_non_existed_instance(self): - actions = ['suspend', 'resume', 'migrate', - 'reset_network', 'inject_network_info', + actions = ['migrate', 'reset_network', 'inject_network_info', 'reset_state', 'migrate_live'] body_map = {'reset_state': {'state': 'active'}, 'migrate_live': {'host': 'hostname', @@ -201,8 +199,7 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_with_locked_instance(self): - actions = ['suspend', 'resume', 'migrate', - 'reset_network', 'inject_network_info'] + actions = ['migrate', 'reset_network', 'inject_network_info'] method_translations = {'migrate': 'resize'} for action in actions: diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_suspend_server.py b/nova/tests/api/openstack/compute/plugins/v3/test_suspend_server.py new file mode 100644 index 0000000000..36e2b89402 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_suspend_server.py @@ -0,0 +1,48 @@ +# Copyright 2013 IBM Corp. +# +# 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 nova.api.openstack.compute.plugins.v3 import suspend_server +from nova.tests.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.api.openstack import fakes + + +class SuspendServerTests(admin_only_action_common.CommonTests): + def setUp(self): + super(SuspendServerTests, self).setUp() + self.controller = suspend_server.SuspendServerController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(suspend_server, 'SuspendServerController', + _fake_controller) + self.app = fakes.wsgi_app_v3(init_only=('servers', + 'os-suspend-server'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_suspend_resume(self): + self._test_actions(['suspend', 'resume']) + + def test_suspend_resume_with_non_existed_instance(self): + self._test_actions_with_non_existed_instance(['suspend', 'resume']) + + def test_suspend_resume_raise_conflict_on_invalid_state(self): + self._test_actions_raise_conflict_on_invalid_state(['suspend', + 'resume']) + + def test_actions_with_locked_instance(self): + self._test_actions_with_locked_instance(['suspend', 'resume']) diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 99ad94bcfe..2d76323406 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -116,8 +116,6 @@ policy_data = """ "compute_extension:admin_actions:migrateLive": "", "compute_extension:admin_actions:resetState": "", "compute_extension:admin_actions:migrate": "", - "compute_extension:v3:os-admin-actions:suspend": "", - "compute_extension:v3:os-admin-actions:resume": "", "compute_extension:v3:os-admin-actions:reset_network": "", "compute_extension:v3:os-admin-actions:inject_network_info": "", "compute_extension:v3:os-admin-actions:create_backup": "", @@ -273,6 +271,8 @@ policy_data = """ "compute_extension:v3:os-simple-tenant-usage:list": "", "compute_extension:unshelve": "", "compute_extension:v3:os-shelve:unshelve": "", + "compute_extension:v3:os-suspend-server:suspend": "", + "compute_extension:v3:os-suspend-server:resume": "", "compute_extension:users": "", "compute_extension:virtual_interfaces": "", "compute_extension:virtual_storage_arrays": "", diff --git a/nova/tests/integrated/v3/api_samples/os-suspend-server/server-post-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-suspend-server/server-post-req.json.tpl new file mode 100644 index 0000000000..d9a7537dfb --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-suspend-server/server-post-req.json.tpl @@ -0,0 +1,10 @@ +{ + "server" : { + "name" : "new-server-test", + "image_ref" : "%(glance_host)s/images/%(image_id)s", + "flavor_ref" : "%(host)s/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + } + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-suspend-server/server-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-suspend-server/server-post-resp.json.tpl new file mode 100644 index 0000000000..eb3f76ebe6 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-suspend-server/server-post-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "server": { + "admin_password": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "%(host)s/v3/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(host)s/servers/%(uuid)s", + "rel": "bookmark" + } + ] + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-resume.json.tpl b/nova/tests/integrated/v3/api_samples/os-suspend-server/server-resume.json.tpl similarity index 100% rename from nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-resume.json.tpl rename to nova/tests/integrated/v3/api_samples/os-suspend-server/server-resume.json.tpl diff --git a/nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-suspend.json.tpl b/nova/tests/integrated/v3/api_samples/os-suspend-server/server-suspend.json.tpl similarity index 100% rename from nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-suspend.json.tpl rename to nova/tests/integrated/v3/api_samples/os-suspend-server/server-suspend.json.tpl diff --git a/nova/tests/integrated/v3/test_admin_actions.py b/nova/tests/integrated/v3/test_admin_actions.py index 2b03d80d69..0d5e961180 100644 --- a/nova/tests/integrated/v3/test_admin_actions.py +++ b/nova/tests/integrated/v3/test_admin_actions.py @@ -31,19 +31,6 @@ class AdminActionsSamplesJsonTest(test_servers.ServersSampleBase): super(AdminActionsSamplesJsonTest, self).setUp() self.uuid = self._post_server() - def test_post_suspend(self): - # Get api samples to suspend server request. - response = self._do_post('servers/%s/action' % self.uuid, - 'admin-actions-suspend', {}) - self.assertEqual(response.status, 202) - - def test_post_resume(self): - # Get api samples to server resume request. - self.test_post_suspend() - response = self._do_post('servers/%s/action' % self.uuid, - 'admin-actions-resume', {}) - self.assertEqual(response.status, 202) - def test_post_migrate(self): # Get api samples to migrate server request. response = self._do_post('servers/%s/action' % self.uuid, diff --git a/nova/tests/integrated/v3/test_suspend_server.py b/nova/tests/integrated/v3/test_suspend_server.py new file mode 100644 index 0000000000..6cca8f2e6d --- /dev/null +++ b/nova/tests/integrated/v3/test_suspend_server.py @@ -0,0 +1,41 @@ +# Copyright 2013 IBM Corp. +# +# 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 nova.tests.integrated.v3 import test_servers + + +class SuspendServerSamplesJsonTest(test_servers.ServersSampleBase): + extension_name = "os-suspend-server" + ctype = 'json' + + def setUp(self): + """setUp Method for SuspendServer api samples extension + + This method creates the server that will be used in each tests + """ + super(SuspendServerSamplesJsonTest, self).setUp() + self.uuid = self._post_server() + + def test_post_suspend(self): + # Get api samples to suspend server request. + response = self._do_post('servers/%s/action' % self.uuid, + 'server-suspend', {}) + self.assertEqual(response.status, 202) + + def test_post_resume(self): + # Get api samples to server resume request. + self.test_post_suspend() + response = self._do_post('servers/%s/action' % self.uuid, + 'server-resume', {}) + self.assertEqual(response.status, 202) diff --git a/setup.cfg b/setup.cfg index ebab19188a..84e415e950 100644 --- a/setup.cfg +++ b/setup.cfg @@ -108,7 +108,7 @@ nova.api.v3.extensions = services = nova.api.openstack.compute.plugins.v3.services:Services shelve = nova.api.openstack.compute.plugins.v3.shelve:Shelve simple_tenant_usage = nova.api.openstack.compute.plugins.v3.simple_tenant_usage:SimpleTenantUsage - user_data = nova.api.openstack.compute.plugins.v3.user_data:UserData + suspend_server = nova.api.openstack.compute.plugins.v3.suspend_server:SuspendServer versions = nova.api.openstack.compute.plugins.v3.versions:Versions nova.api.v3.extensions.server.create =