Adds lock server extension for V3 API

Moves the lock/unlock server functionality out of admin_actions into
its own extension. This part of the larger
blueprint v3-api-admin-actions-split allows more selective enablement of
features contained in the admin actions extension.

Some setup work is done in the tests directory with an
admin_only_action_common.py file. This allows tests which are
split out from test_admin_actions (as their corresponding features
are separated from the admin_actions extension) can continue to
share code.

Note that XML api samples are no longer generated because
bp remove-v3-xml-api has been approved.

Partially implements bp v3-api-admin-actions-split
DocImpact: Adds os-lock-server extension and moves lock/unlock
functionality out of os-admin-actions into this new extension

Change-Id: Ie4b6e856c2f5c33de5575aa8666e0b2784b58d05
This commit is contained in:
Chris Yeoh 2013-11-25 22:36:32 +10:30
parent 475fa8ce31
commit 237e990926
22 changed files with 352 additions and 69 deletions

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<lock />

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<unlock />

View File

@ -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=="
}
]
}
}

View File

@ -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"
}
]
}
}

View File

@ -43,8 +43,6 @@
"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:lock": "rule:admin_or_owner",
"compute_extension:v3:os-admin-actions:unlock": "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",
@ -177,6 +175,9 @@
"compute_extension:v3:keypairs:show": "",
"compute_extension:v3:keypairs:create": "",
"compute_extension:v3:keypairs:delete": "",
"compute_extension:v3:os-lock-server:discoverable": "",
"compute_extension:v3:os-lock-server:lock": "rule:admin_or_owner",
"compute_extension:v3:os-lock-server:unlock": "rule:admin_or_owner",
"compute_extension:multinic": "",
"compute_extension:v3:os-multinic": "",
"compute_extension:v3:os-multinic:discoverable": "",

View File

@ -174,32 +174,6 @@ class AdminActionsController(wsgi.Controller):
raise exc.HTTPConflict(explanation=e.format_message())
return webob.Response(status_int=202)
@extensions.expected_errors(404)
@wsgi.action('lock')
def _lock(self, req, id, body):
"""Lock a server instance."""
context = req.environ['nova.context']
authorize(context, 'lock')
try:
instance = self.compute_api.get(context, id, want_objects=True)
self.compute_api.lock(context, instance)
except exception.InstanceNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
return webob.Response(status_int=202)
@extensions.expected_errors(404)
@wsgi.action('unlock')
def _unlock(self, req, id, body):
"""Unlock a server instance."""
context = req.environ['nova.context']
authorize(context, 'unlock')
try:
instance = self.compute_api.get(context, id, want_objects=True)
self.compute_api.unlock(context, instance)
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('create_backup')
def _create_backup(self, req, id, body):

View File

@ -0,0 +1,79 @@
# 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 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-lock-server"
def authorize(context, action_name):
action = 'v3:%s:%s' % (ALIAS, action_name)
extensions.extension_authorizer('compute', action)(context)
class LockServerController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(LockServerController, self).__init__(*args, **kwargs)
self.compute_api = compute.API()
@extensions.expected_errors(404)
@wsgi.action('lock')
def _lock(self, req, id, body):
"""Lock a server instance."""
context = req.environ['nova.context']
authorize(context, 'lock')
try:
instance = self.compute_api.get(context, id, want_objects=True)
self.compute_api.lock(context, instance)
except exception.InstanceNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
return webob.Response(status_int=202)
@extensions.expected_errors(404)
@wsgi.action('unlock')
def _unlock(self, req, id, body):
"""Unlock a server instance."""
context = req.environ['nova.context']
authorize(context, 'unlock')
try:
instance = self.compute_api.get(context, id, want_objects=True)
self.compute_api.unlock(context, instance)
except exception.InstanceNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
return webob.Response(status_int=202)
class LockServer(extensions.V3APIExtensionBase):
"""Enable lock/unlock server actions."""
name = "LockServer"
alias = ALIAS
namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS
version = 1
def get_controller_extensions(self):
controller = LockServerController()
extension = extensions.ControllerExtension(self, 'servers', controller)
return [extension]
def get_resources(self):
return []

View File

@ -0,0 +1,104 @@
# 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 nova.compute import vm_states
import nova.context
from nova import exception
from nova.openstack.common import jsonutils
from nova.openstack.common import timeutils
from nova.openstack.common import uuidutils
from nova import test
from nova.tests import fake_instance
class CommonMixin(object):
def setUp(self):
super(CommonMixin, self).setUp()
self.compute_api = None
self.context = nova.context.RequestContext('fake', 'fake')
def _make_request(self, url, body):
req = webob.Request.blank('/v3' + url)
req.method = 'POST'
req.body = jsonutils.dumps(body)
req.content_type = 'application/json'
return req.get_response(self.app)
def _stub_instance_get(self, uuid=None):
if uuid is None:
uuid = uuidutils.generate_uuid()
instance = fake_instance.fake_instance_obj(self.context,
id=1, uuid=uuid, vm_state=vm_states.ACTIVE,
task_state=None, launched_at=timeutils.utcnow())
self.compute_api.get(self.context, uuid,
want_objects=True).AndReturn(instance)
return instance
def _stub_instance_get_failure(self, exc_info, uuid=None):
if uuid is None:
uuid = uuidutils.generate_uuid()
self.compute_api.get(self.context, uuid,
want_objects=True).AndRaise(exc_info)
return uuid
def _test_non_existing_instance(self, action, body_map=None):
uuid = uuidutils.generate_uuid()
self._stub_instance_get_failure(
exception.InstanceNotFound(instance_id=uuid), uuid=uuid)
self.mox.ReplayAll()
res = self._make_request('/servers/%s/action' % uuid,
{action: body_map.get(action)})
self.assertEqual(404, 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()
def _test_action(self, action, body=None, method=None):
if method is None:
method = action
instance = self._stub_instance_get()
getattr(self.compute_api, method)(self.context, instance)
self.mox.ReplayAll()
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
# 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={}):
for action in actions:
method = method_translations.get(action)
self.mox.StubOutWithMock(self.compute_api, method or action)
self._test_action(action, method=method)
# Re-mock this.
self.mox.StubOutWithMock(self.compute_api, 'get')
def _test_actions_with_non_existed_instance(self, actions, body_map={}):
for action in actions:
self._test_non_existing_instance(action,
body_map=body_map)
# Re-mock this.
self.mox.StubOutWithMock(self.compute_api, 'get')

View File

@ -157,8 +157,7 @@ class CommonMixin(object):
class AdminActionsTest(CommonMixin, test.NoDBTestCase):
def test_actions(self):
actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate',
'reset_network', 'inject_network_info', 'lock',
'unlock']
'reset_network', 'inject_network_info']
method_translations = {'migrate': 'resize'}
for action in actions:
@ -190,8 +189,8 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase):
def test_actions_with_non_existed_instance(self):
actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate',
'reset_network', 'inject_network_info', 'lock',
'unlock', 'reset_state', 'migrate_live']
'reset_network', 'inject_network_info',
'reset_state', 'migrate_live']
body_map = {'reset_state': {'state': 'active'},
'migrate_live': {'host': 'hostname',
'block_migration': False,
@ -332,20 +331,6 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase):
self._test_migrate_live_failed_with_exception(
exception.MigrationPreCheckError(reason=''))
def test_unlock_not_authorized(self):
self.mox.StubOutWithMock(self.compute_api, 'unlock')
instance = self._stub_instance_get()
self.compute_api.unlock(self.context, instance).AndRaise(
exception.PolicyNotAuthorized(action='unlock'))
self.mox.ReplayAll()
res = self._make_request('/servers/%s/action' % instance['uuid'],
{'unlock': None})
self.assertEqual(403, res.status_int)
class CreateBackupTests(CommonMixin, test.NoDBTestCase):
def setUp(self):

View File

@ -0,0 +1,56 @@
# 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 lock_server
from nova import exception
from nova.tests.api.openstack.compute.plugins.v3 import \
admin_only_action_common
from nova.tests.api.openstack import fakes
class LockServerTests(admin_only_action_common.CommonTests):
def setUp(self):
super(LockServerTests, self).setUp()
self.controller = lock_server.LockServerController()
self.compute_api = self.controller.compute_api
def _fake_controller(*args, **kwargs):
return self.controller
self.stubs.Set(lock_server, 'LockServerController',
_fake_controller)
self.app = fakes.wsgi_app_v3(init_only=('servers',
'os-lock-server'),
fake_auth_context=self.context)
self.mox.StubOutWithMock(self.compute_api, 'get')
def test_lock_unlock(self):
self._test_actions(['lock', 'unlock'])
def test_lock_unlock_with_non_existed_instance(self):
self._test_actions_with_non_existed_instance(['lock', 'unlock'])
def test_unlock_not_authorized(self):
self.mox.StubOutWithMock(self.compute_api, 'unlock')
instance = self._stub_instance_get()
self.compute_api.unlock(self.context, instance).AndRaise(
exception.PolicyNotAuthorized(action='unlock'))
self.mox.ReplayAll()
res = self._make_request('/servers/%s/action' % instance.uuid,
{'unlock': None})
self.assertEqual(403, res.status_int)

View File

@ -120,8 +120,6 @@ policy_data = """
"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:lock": "",
"compute_extension:v3:os-admin-actions:unlock": "",
"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": "",
@ -234,6 +232,8 @@ policy_data = """
"compute_extension:v3:keypairs:show": "",
"compute_extension:v3:keypairs:create": "",
"compute_extension:v3:keypairs:delete": "",
"compute_extension:v3:os-lock-server:lock": "",
"compute_extension:v3:os-lock-server:unlock": "",
"compute_extension:multinic": "",
"compute_extension:v3:os-multinic": "",
"compute_extension:networks": "",

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<lock />

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<unlock />

View File

@ -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=="
}
]
}
}

View File

@ -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"
}
]
}
}

View File

@ -75,19 +75,6 @@ class AdminActionsSamplesJsonTest(test_servers.ServersSampleBase):
'admin-actions-inject-network-info', {})
self.assertEqual(response.status, 202)
def test_post_lock_server(self):
# Get api samples to lock server request.
response = self._do_post('servers/%s/action' % self.uuid,
'admin-actions-lock-server', {})
self.assertEqual(response.status, 202)
def test_post_unlock_server(self):
# Get api samples to unlock server request.
self.test_post_lock_server()
response = self._do_post('servers/%s/action' % self.uuid,
'admin-actions-unlock-server', {})
self.assertEqual(response.status, 202)
def test_post_backup_server(self):
# Get api samples to backup server request.
def image_details(self, context, **kwargs):

View File

@ -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 LockServerSamplesJsonTest(test_servers.ServersSampleBase):
extension_name = "os-lock-server"
def setUp(self):
"""setUp Method for LockServer api samples extension
This method creates the server that will be used in each tests
"""
super(LockServerSamplesJsonTest, self).setUp()
self.uuid = self._post_server()
def test_post_lock_server(self):
# Get api samples to lock server request.
response = self._do_post('servers/%s/action' % self.uuid,
'lock-server', {})
self.assertEqual(response.status, 202)
def test_post_unlock_server(self):
# Get api samples to unlock server request.
self.test_post_lock_server()
response = self._do_post('servers/%s/action' % self.uuid,
'unlock-server', {})
self.assertEqual(response.status, 202)

View File

@ -88,6 +88,7 @@ nova.api.v3.extensions =
ips = nova.api.openstack.compute.plugins.v3.ips:IPs
instance_usage_audit_log = nova.api.openstack.compute.plugins.v3.instance_usage_audit_log:InstanceUsageAuditLog
keypairs = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs
lock_server = nova.api.openstack.compute.plugins.v3.lock_server:LockServer
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