diff --git a/doc/v3/api_samples/os-admin-actions/admin-actions-pause.json b/doc/v3/api_samples/os-pause-server/pause-server.json similarity index 100% rename from doc/v3/api_samples/os-admin-actions/admin-actions-pause.json rename to doc/v3/api_samples/os-pause-server/pause-server.json diff --git a/doc/v3/api_samples/os-pause-server/server-post-req.json b/doc/v3/api_samples/os-pause-server/server-post-req.json new file mode 100644 index 000000000000..30851df41a56 --- /dev/null +++ b/doc/v3/api_samples/os-pause-server/server-post-req.json @@ -0,0 +1,16 @@ +{ + "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" + }, + "personality" : [ + { + "path" : "/etc/banner.txt", + "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-pause-server/server-post-resp.json b/doc/v3/api_samples/os-pause-server/server-post-resp.json new file mode 100644 index 000000000000..270cb8463484 --- /dev/null +++ b/doc/v3/api_samples/os-pause-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-unpause.json b/doc/v3/api_samples/os-pause-server/unpause-server.json similarity index 100% rename from doc/v3/api_samples/os-admin-actions/admin-actions-unpause.json rename to doc/v3/api_samples/os-pause-server/unpause-server.json diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 4ada50c84d3b..0d24e1d360c5 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:pause": "rule:admin_or_owner", - "compute_extension:v3:os-admin-actions:unpause": "rule:admin_or_owner", "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", @@ -184,6 +182,9 @@ "compute_extension:networks": "rule:admin_api", "compute_extension:networks:view": "", "compute_extension:networks_associate": "rule:admin_api", + "compute_extension:v3:os-pause-server:discoverable": "", + "compute_extension:v3:os-pause-server:pause": "rule:admin_or_owner", + "compute_extension:v3:os-pause-server:unpause": "rule:admin_or_owner", "compute_extension:v3:os-pci:pci_servers": "", "compute_extension:v3:os-pci:discoverable": "", "compute_extension:quotas:show": "", diff --git a/nova/api/openstack/compute/plugins/v3/admin_actions.py b/nova/api/openstack/compute/plugins/v3/admin_actions.py index 24ea6eb429c4..2a5e2a548925 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('pause') - def _pause(self, req, id, body): - """Permit Admins to pause the server.""" - ctxt = req.environ['nova.context'] - authorize(ctxt, 'pause') - try: - server = self.compute_api.get(ctxt, id, want_objects=True) - self.compute_api.pause(ctxt, 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, - 'pause') - 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('unpause') - def _unpause(self, req, id, body): - """Permit Admins to unpause the server.""" - ctxt = req.environ['nova.context'] - authorize(ctxt, 'unpause') - try: - server = self.compute_api.get(ctxt, id, want_objects=True) - self.compute_api.unpause(ctxt, 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, - 'unpause') - 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('suspend') def _suspend(self, req, id, body): diff --git a/nova/api/openstack/compute/plugins/v3/pause_server.py b/nova/api/openstack/compute/plugins/v3/pause_server.py new file mode 100644 index 000000000000..3b01a1dc0d29 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/pause_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-pause-server" + + +def authorize(context, action_name): + action = 'v3:%s:%s' % (ALIAS, action_name) + extensions.extension_authorizer('compute', action)(context) + + +class PauseServerController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(PauseServerController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + @extensions.expected_errors((404, 409)) + @wsgi.action('pause') + def _pause(self, req, id, body): + """Permit Admins to pause the server.""" + ctxt = req.environ['nova.context'] + authorize(ctxt, 'pause') + try: + server = self.compute_api.get(ctxt, id, want_objects=True) + self.compute_api.pause(ctxt, 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, + 'pause') + 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('unpause') + def _unpause(self, req, id, body): + """Permit Admins to unpause the server.""" + ctxt = req.environ['nova.context'] + authorize(ctxt, 'unpause') + try: + server = self.compute_api.get(ctxt, id, want_objects=True) + self.compute_api.unpause(ctxt, 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, + 'unpause') + except exception.InstanceNotFound as e: + raise exc.HTTPNotFound(explanation=e.format_message()) + return webob.Response(status_int=202) + + +class PauseServer(extensions.V3APIExtensionBase): + """Enable pause/unpause server actions.""" + + name = "PauseServer" + alias = ALIAS + namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS + version = 1 + + def get_controller_extensions(self): + controller = PauseServerController() + extension = extensions.ControllerExtension(self, 'servers', controller) + return [extension] + + def get_resources(self): + return [] diff --git a/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py b/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py index 7884dcf5f372..89601355d202 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py +++ b/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py @@ -78,7 +78,7 @@ class CommonMixin(object): self.mox.ReplayAll() - res = self._make_request('/servers/%s/action' % instance['uuid'], + res = self._make_request('/servers/%s/action' % instance.uuid, {action: None}) self.assertEqual(202, res.status_int) # Do these here instead of tearDown because this method is called @@ -86,6 +86,54 @@ class CommonMixin(object): self.mox.VerifyAll() self.mox.UnsetStubs() + def _test_invalid_state(self, action, method=None, body_map=None, + compute_api_args_map=None): + if method is None: + method = action + if body_map is None: + body_map = {} + if compute_api_args_map is None: + compute_api_args_map = {} + + instance = self._stub_instance_get() + + args, kwargs = compute_api_args_map.get(action, ((), {})) + + getattr(self.compute_api, method)(self.context, instance, + *args, **kwargs).AndRaise( + exception.InstanceInvalidState( + attr='vm_state', instance_uuid=instance.uuid, + state='foo', method=method)) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {action: body_map.get(action)}) + self.assertEqual(409, res.status_int) + self.assertIn("Cannot \'%s\' while instance" % action, res.body) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_locked_instance(self, action, method=None): + if method is None: + method = action + + instance = self._stub_instance_get() + getattr(self.compute_api, method)(self.context, instance).AndRaise( + exception.InstanceIsLocked(instance_uuid=instance.uuid)) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {action: None}) + self.assertEqual(409, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + class CommonTests(CommonMixin, test.NoDBTestCase): def _test_actions(self, actions, method_translations={}): @@ -102,3 +150,23 @@ class CommonTests(CommonMixin, test.NoDBTestCase): body_map=body_map) # Re-mock this. self.mox.StubOutWithMock(self.compute_api, 'get') + + def _test_actions_raise_conflict_on_invalid_state( + self, actions, method_translations={}, body_map={}, args_map={}): + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_invalid_state(action, method=method, + body_map=body_map, + compute_api_args_map=args_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def _test_actions_with_locked_instance(self, actions, + method_translations={}): + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_locked_instance(action, method=method) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') 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 41606a9f4ac1..4be16d4c3a60 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,7 +156,7 @@ class CommonMixin(object): class AdminActionsTest(CommonMixin, test.NoDBTestCase): def test_actions(self): - actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate', + actions = ['suspend', 'resume', 'migrate', 'reset_network', 'inject_network_info'] method_translations = {'migrate': 'resize'} @@ -168,8 +168,7 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_raise_conflict_on_invalid_state(self): - actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate', - 'migrate_live'] + actions = ['suspend', 'resume', 'migrate', 'migrate_live'] method_translations = {'migrate': 'resize', 'migrate_live': 'live_migrate'} @@ -188,7 +187,7 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_with_non_existed_instance(self): - actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate', + actions = ['suspend', 'resume', 'migrate', 'reset_network', 'inject_network_info', 'reset_state', 'migrate_live'] body_map = {'reset_state': {'state': 'active'}, @@ -202,7 +201,7 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_with_locked_instance(self): - actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate', + actions = ['suspend', 'resume', 'migrate', 'reset_network', 'inject_network_info'] method_translations = {'migrate': 'resize'} diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_pause_server.py b/nova/tests/api/openstack/compute/plugins/v3/test_pause_server.py new file mode 100644 index 000000000000..0f279295061b --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_pause_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 pause_server +from nova.tests.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.api.openstack import fakes + + +class PauseServerTests(admin_only_action_common.CommonTests): + def setUp(self): + super(PauseServerTests, self).setUp() + self.controller = pause_server.PauseServerController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(pause_server, 'PauseServerController', + _fake_controller) + self.app = fakes.wsgi_app_v3(init_only=('servers', + 'os-pause-server'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_pause_unpause(self): + self._test_actions(['pause', 'unpause']) + + def test_pause_unpause_with_non_existed_instance(self): + self._test_actions_with_non_existed_instance(['pause', 'unpause']) + + def test_pause_unpause_raise_conflict_on_invalid_state(self): + self._test_actions_raise_conflict_on_invalid_state(['pause', + 'unpause']) + + def test_actions_with_locked_instance(self): + self._test_actions_with_locked_instance(['pause', 'unpause']) diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index ef9d71147a71..99ad94bcfe32 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:pause": "", - "compute_extension:v3:os-admin-actions:unpause": "", "compute_extension:v3:os-admin-actions:suspend": "", "compute_extension:v3:os-admin-actions:resume": "", "compute_extension:v3:os-admin-actions:reset_network": "", @@ -240,6 +238,8 @@ policy_data = """ "compute_extension:networks:view": "", "compute_extension:networks_associate": "", "compute_extension:os-tenant-networks": "", + "compute_extension:v3:os-pause-server:pause": "", + "compute_extension:v3:os-pause-server:unpause": "", "compute_extension:v3:os-pci:pci_servers": "", "compute_extension:quotas:show": "", "compute_extension:quotas:update": "", diff --git a/nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-pause.json.tpl b/nova/tests/integrated/v3/api_samples/os-pause-server/pause-server.json.tpl similarity index 100% rename from nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-pause.json.tpl rename to nova/tests/integrated/v3/api_samples/os-pause-server/pause-server.json.tpl diff --git a/nova/tests/integrated/v3/api_samples/os-pause-server/server-post-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-pause-server/server-post-req.json.tpl new file mode 100644 index 000000000000..e6c046ceb4e9 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-pause-server/server-post-req.json.tpl @@ -0,0 +1,16 @@ +{ + "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" + }, + "personality" : [ + { + "path" : "/etc/banner.txt", + "contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ] + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-pause-server/server-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-pause-server/server-post-resp.json.tpl new file mode 100644 index 000000000000..eb3f76ebe6d3 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-pause-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-unpause.json.tpl b/nova/tests/integrated/v3/api_samples/os-pause-server/unpause-server.json.tpl similarity index 100% rename from nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-unpause.json.tpl rename to nova/tests/integrated/v3/api_samples/os-pause-server/unpause-server.json.tpl diff --git a/nova/tests/integrated/v3/test_admin_actions.py b/nova/tests/integrated/v3/test_admin_actions.py index 34571637b7a2..2b03d80d69f4 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_pause(self): - # Get api samples to pause server request. - response = self._do_post('servers/%s/action' % self.uuid, - 'admin-actions-pause', {}) - self.assertEqual(response.status, 202) - - def test_post_unpause(self): - # Get api samples to unpause server request. - self.test_post_pause() - response = self._do_post('servers/%s/action' % self.uuid, - 'admin-actions-unpause', {}) - self.assertEqual(response.status, 202) - def test_post_suspend(self): # Get api samples to suspend server request. response = self._do_post('servers/%s/action' % self.uuid, diff --git a/nova/tests/integrated/v3/test_pause_server.py b/nova/tests/integrated/v3/test_pause_server.py new file mode 100644 index 000000000000..16ef351dcaa1 --- /dev/null +++ b/nova/tests/integrated/v3/test_pause_server.py @@ -0,0 +1,40 @@ +# 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 PauseServerSamplesJsonTest(test_servers.ServersSampleBase): + extension_name = "os-pause-server" + + def setUp(self): + """setUp Method for PauseServer api samples extension + + This method creates the server that will be used in each test + """ + super(PauseServerSamplesJsonTest, self).setUp() + self.uuid = self._post_server() + + def test_post_pause(self): + # Get api samples to pause server request. + response = self._do_post('servers/%s/action' % self.uuid, + 'pause-server', {}) + self.assertEqual(response.status, 202) + + def test_post_unpause(self): + # Get api samples to unpause server request. + self.test_post_pause() + response = self._do_post('servers/%s/action' % self.uuid, + 'unpause-server', {}) + self.assertEqual(response.status, 202) diff --git a/setup.cfg b/setup.cfg index 729158a24c72..ebab19188a4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,6 +92,7 @@ nova.api.v3.extensions = migrations = nova.api.openstack.compute.plugins.v3.migrations:Migrations multinic = nova.api.openstack.compute.plugins.v3.multinic:Multinic multiple_create = nova.api.openstack.compute.plugins.v3.multiple_create:MultipleCreate + pause_server = nova.api.openstack.compute.plugins.v3.pause_server:PauseServer pci = nova.api.openstack.compute.plugins.v3.pci:Pci quota_classes = nova.api.openstack.compute.plugins.v3.quota_classes:QuotaClasses quota_sets = nova.api.openstack.compute.plugins.v3.quota_sets:QuotaSets