Implement server action API(part 1)
In this patch start and stop action are implemented. Change-Id: I8381b75199b6307897257a74675eeeda91ba66b5
This commit is contained in:
parent
08e868823f
commit
fbaaf20324
@ -98,6 +98,65 @@ def _safe_operation(operation_name):
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Wrapper of all OpenStack service clients
|
||||
|
||||
Client works as a wrapper of all OpenStack service clients so you can
|
||||
operate all kinds of resources by only interacting with Client. Client
|
||||
provides five methods to operate resources:
|
||||
create_resources
|
||||
delete_resources
|
||||
get_resources
|
||||
list_resources
|
||||
action_resources
|
||||
|
||||
Take create_resources as an example to show how Client works. When
|
||||
create_resources is called, it gets the corresponding service handler
|
||||
according to the resource type. Service handlers are defined in
|
||||
resource_handle.py and each service has one. Each handler has the
|
||||
following methods:
|
||||
handle_create
|
||||
handle_delete
|
||||
handle_get
|
||||
handle_list
|
||||
handle_action
|
||||
It's obvious that create_resources is mapped to handle_create(for port,
|
||||
handle_create in NeutronResourceHandle is called).
|
||||
|
||||
Not all kinds of resources support the above five operations(or not
|
||||
supported yet by Tricircle), so each service handler has a
|
||||
support_resource field to specify the resources and operations it
|
||||
supports, like:
|
||||
'port': LIST | CREATE | DELETE | GET
|
||||
This line means that NeutronResourceHandle supports list, create, delete
|
||||
and get operations for port resource. To support more resources or make a
|
||||
resource support more operations, register them in support_resource.
|
||||
|
||||
Dig into "handle_xxx" you can find that it will call methods in each
|
||||
OpenStack service client finally. Calling handle_create for port will
|
||||
result in calling create_port in neutronclient module.
|
||||
|
||||
Current "handle_xxx" implementation constructs method name by resource
|
||||
and operation type and uses getattr to dynamically load method from
|
||||
OpenStack service client so it can cover most of the cases. Supporting a
|
||||
new kind of resource or making a resource support a new kind of operation
|
||||
is simply to register an entry in support_resource as described above.
|
||||
But if some special cases occur later, modifying "handle_xxx" is needed.
|
||||
|
||||
Also, pay attention to action operation since you need to check the
|
||||
implementation of the OpenStack service client to know what the method
|
||||
name of the action is and what parameters the method has. In the comment of
|
||||
action_resources you can find that for each action, there is one line to
|
||||
describe the method name and parameters like:
|
||||
aggregate -> add_host -> aggregate, host -> none
|
||||
This line means that for aggregate resource, novaclient module has an
|
||||
add_host method and it has two position parameters and no key parameter.
|
||||
For simplicity, action name and method name are the same.
|
||||
|
||||
One more thing to mention, Client registers a partial function
|
||||
(operation)_(resource)s for each operation and each resource. For example,
|
||||
you can call create_resources(self, resource, cxt, body) directly to create
|
||||
a network, or use create_networks(self, cxt, body) for short.
|
||||
"""
|
||||
def __init__(self, pod_name=None):
|
||||
self.auth_url = cfg.CONF.client.auth_url
|
||||
self.resource_service_map = {}
|
||||
@ -458,6 +517,8 @@ class Client(object):
|
||||
server_volume -> create_server_volume
|
||||
-> server_id, volume_id, device=None
|
||||
-> none
|
||||
server -> start -> server_id -> none
|
||||
server -> stop -> server_id -> none
|
||||
--------------------------
|
||||
:return: None
|
||||
:raises: EndpointNotAvailable
|
||||
|
@ -202,10 +202,14 @@ class NeutronResourceHandle(ResourceHandle):
|
||||
'neutron', client.httpclient.endpoint_url)
|
||||
|
||||
|
||||
def _convert_into_with_meta(item, resp):
|
||||
return resp, item
|
||||
|
||||
|
||||
class NovaResourceHandle(ResourceHandle):
|
||||
service_type = cons.ST_NOVA
|
||||
support_resource = {'flavor': LIST,
|
||||
'server': LIST | CREATE | GET,
|
||||
'server': LIST | CREATE | GET | ACTION,
|
||||
'aggregate': LIST | CREATE | DELETE | ACTION,
|
||||
'server_volume': ACTION}
|
||||
|
||||
@ -288,6 +292,9 @@ class NovaResourceHandle(ResourceHandle):
|
||||
client = self._get_client(cxt)
|
||||
collection = '%ss' % resource
|
||||
resource_manager = getattr(client, collection)
|
||||
resource_manager.convert_into_with_meta = _convert_into_with_meta
|
||||
# NOTE(zhiyuan) yes, this is a dirty hack. but the original
|
||||
# implementation hides response object which is needed
|
||||
return getattr(resource_manager, action)(*args, **kwargs)
|
||||
except r_exceptions.ConnectTimeout:
|
||||
self.endpoint_url = None
|
||||
|
99
tricircle/nova_apigw/controllers/action.py
Normal file
99
tricircle/nova_apigw/controllers/action.py
Normal file
@ -0,0 +1,99 @@
|
||||
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 pecan
|
||||
from pecan import expose
|
||||
from pecan import rest
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
import tricircle.common.client as t_client
|
||||
from tricircle.common import constants
|
||||
import tricircle.common.context as t_context
|
||||
from tricircle.common.i18n import _
|
||||
import tricircle.db.api as db_api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionController(rest.RestController):
|
||||
|
||||
def __init__(self, project_id, server_id):
|
||||
self.project_id = project_id
|
||||
self.server_id = server_id
|
||||
self.clients = {constants.TOP: t_client.Client()}
|
||||
self.handle_map = {
|
||||
'os-start': self._handle_start,
|
||||
'os-stop': self._handle_stop
|
||||
}
|
||||
|
||||
def _get_client(self, pod_name=constants.TOP):
|
||||
if pod_name not in self.clients:
|
||||
self.clients[pod_name] = t_client.Client(pod_name)
|
||||
return self.clients[pod_name]
|
||||
|
||||
def _handle_start(self, context, pod_name, body):
|
||||
client = self._get_client(pod_name)
|
||||
return client.action_servers(context, 'start', self.server_id)
|
||||
|
||||
def _handle_stop(self, context, pod_name, body):
|
||||
client = self._get_client(pod_name)
|
||||
return client.action_servers(context, 'stop', self.server_id)
|
||||
|
||||
@staticmethod
|
||||
def _format_error(code, message):
|
||||
pecan.response.status = code
|
||||
# format error message in this form so nova client can
|
||||
# correctly parse it
|
||||
return {'Error': {'message': message, 'code': code}}
|
||||
|
||||
@expose(generic=True, template='json')
|
||||
def post(self, **kw):
|
||||
context = t_context.extract_context_from_environ()
|
||||
|
||||
action_handle = None
|
||||
action_type = None
|
||||
for _type in self.handle_map:
|
||||
if _type in kw:
|
||||
action_handle = self.handle_map[_type]
|
||||
action_type = _type
|
||||
if not action_handle:
|
||||
return self._format_error(400, _('Server action not supported'))
|
||||
|
||||
server_mappings = db_api.get_bottom_mappings_by_top_id(
|
||||
context, self.server_id, constants.RT_SERVER)
|
||||
if not server_mappings:
|
||||
return self._format_error(404, _('Server not found'))
|
||||
|
||||
pod_name = server_mappings[0][0]['pod_name']
|
||||
try:
|
||||
resp, body = action_handle(context, pod_name, kw)
|
||||
pecan.response.status = resp.status_code
|
||||
if not body:
|
||||
return pecan.response
|
||||
else:
|
||||
return body
|
||||
except Exception as e:
|
||||
code = 500
|
||||
message = _('Action %(action)s on server %(server_id)s fails') % {
|
||||
'action': action_type,
|
||||
'server_id': self.server_id}
|
||||
if hasattr(e, 'code'):
|
||||
code = e.code
|
||||
ex_message = str(e)
|
||||
if ex_message:
|
||||
message = ex_message
|
||||
LOG.error(message)
|
||||
return self._format_error(code, message)
|
@ -25,6 +25,7 @@ import webob.exc as web_exc
|
||||
|
||||
from tricircle.common import context as ctx
|
||||
from tricircle.common import xrpcapi
|
||||
from tricircle.nova_apigw.controllers import action
|
||||
from tricircle.nova_apigw.controllers import aggregate
|
||||
from tricircle.nova_apigw.controllers import flavor
|
||||
from tricircle.nova_apigw.controllers import image
|
||||
@ -95,7 +96,8 @@ class V21Controller(object):
|
||||
'limits': quota_sets.LimitsController,
|
||||
}
|
||||
self.server_sub_controller = {
|
||||
'os-volume_attachments': volume.VolumeController
|
||||
'os-volume_attachments': volume.VolumeController,
|
||||
'action': action.ActionController
|
||||
}
|
||||
|
||||
def _get_resource_controller(self, project_id, remainder):
|
||||
|
169
tricircle/tests/unit/nova_apigw/controllers/test_action.py
Normal file
169
tricircle/tests/unit/nova_apigw/controllers/test_action.py
Normal file
@ -0,0 +1,169 @@
|
||||
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 mock import patch
|
||||
import pecan
|
||||
import unittest
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from tricircle.common import client
|
||||
from tricircle.common import constants
|
||||
from tricircle.common import context
|
||||
from tricircle.common import exceptions
|
||||
from tricircle.db import api
|
||||
from tricircle.db import core
|
||||
from tricircle.db import models
|
||||
from tricircle.nova_apigw.controllers import action
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
def __new__(cls, code=500):
|
||||
cls.status = code
|
||||
cls.status_code = code
|
||||
return super(FakeResponse, cls).__new__(cls)
|
||||
|
||||
|
||||
class ActionTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
core.initialize()
|
||||
core.ModelBase.metadata.create_all(core.get_engine())
|
||||
self.context = context.get_admin_context()
|
||||
self.project_id = 'test_project'
|
||||
self.controller = action.ActionController(self.project_id, '')
|
||||
|
||||
def _prepare_pod(self, bottom_pod_num=1):
|
||||
t_pod = {'pod_id': 't_pod_uuid', 'pod_name': 't_region',
|
||||
'az_name': ''}
|
||||
api.create_pod(self.context, t_pod)
|
||||
b_pods = []
|
||||
if bottom_pod_num == 1:
|
||||
b_pod = {'pod_id': 'b_pod_uuid', 'pod_name': 'b_region',
|
||||
'az_name': 'b_az'}
|
||||
api.create_pod(self.context, b_pod)
|
||||
b_pods.append(b_pod)
|
||||
else:
|
||||
for i in xrange(1, bottom_pod_num + 1):
|
||||
b_pod = {'pod_id': 'b_pod_%d_uuid' % i,
|
||||
'pod_name': 'b_region_%d' % i,
|
||||
'az_name': 'b_az_%d' % i}
|
||||
api.create_pod(self.context, b_pod)
|
||||
b_pods.append(b_pod)
|
||||
return t_pod, b_pods
|
||||
|
||||
def _prepare_server(self, pod):
|
||||
t_server_id = uuidutils.generate_uuid()
|
||||
b_server_id = t_server_id
|
||||
with self.context.session.begin():
|
||||
core.create_resource(
|
||||
self.context, models.ResourceRouting,
|
||||
{'top_id': t_server_id, 'bottom_id': b_server_id,
|
||||
'pod_id': pod['pod_id'], 'project_id': self.project_id,
|
||||
'resource_type': constants.RT_SERVER})
|
||||
return t_server_id
|
||||
|
||||
@patch.object(pecan, 'response', new=FakeResponse)
|
||||
@patch.object(context, 'extract_context_from_environ')
|
||||
def test_action_not_supported(self, mock_context):
|
||||
mock_context.return_value = self.context
|
||||
|
||||
body = {'unsupported_action': ''}
|
||||
res = self.controller.post(**body)
|
||||
self.assertEqual('Server action not supported',
|
||||
res['Error']['message'])
|
||||
self.assertEqual(400, res['Error']['code'])
|
||||
|
||||
@patch.object(pecan, 'response', new=FakeResponse)
|
||||
@patch.object(context, 'extract_context_from_environ')
|
||||
def test_action_server_not_found(self, mock_context):
|
||||
mock_context.return_value = self.context
|
||||
|
||||
body = {'os-start': ''}
|
||||
res = self.controller.post(**body)
|
||||
self.assertEqual('Server not found', res['Error']['message'])
|
||||
self.assertEqual(404, res['Error']['code'])
|
||||
|
||||
@patch.object(pecan, 'response', new=FakeResponse)
|
||||
@patch.object(client.Client, 'action_resources')
|
||||
@patch.object(context, 'extract_context_from_environ')
|
||||
def test_action_exception(self, mock_context, mock_action):
|
||||
mock_context.return_value = self.context
|
||||
|
||||
t_pod, b_pods = self._prepare_pod()
|
||||
t_server_id = self._prepare_server(b_pods[0])
|
||||
self.controller.server_id = t_server_id
|
||||
|
||||
mock_action.side_effect = exceptions.HTTPForbiddenError(
|
||||
msg='Server operation forbidden')
|
||||
body = {'os-start': ''}
|
||||
res = self.controller.post(**body)
|
||||
# this is the message of HTTPForbiddenError exception
|
||||
self.assertEqual('Server operation forbidden', res['Error']['message'])
|
||||
# this is the code of HTTPForbiddenError exception
|
||||
self.assertEqual(403, res['Error']['code'])
|
||||
|
||||
mock_action.side_effect = exceptions.ServiceUnavailable
|
||||
body = {'os-start': ''}
|
||||
res = self.controller.post(**body)
|
||||
# this is the message of ServiceUnavailable exception
|
||||
self.assertEqual('The service is unavailable', res['Error']['message'])
|
||||
# code is 500 by default
|
||||
self.assertEqual(500, res['Error']['code'])
|
||||
|
||||
mock_action.side_effect = Exception
|
||||
body = {'os-start': ''}
|
||||
res = self.controller.post(**body)
|
||||
# use default message if exception's message is empty
|
||||
self.assertEqual('Action os-start on server %s fails' % t_server_id,
|
||||
res['Error']['message'])
|
||||
# code is 500 by default
|
||||
self.assertEqual(500, res['Error']['code'])
|
||||
|
||||
@patch.object(pecan, 'response', new=FakeResponse)
|
||||
@patch.object(client.Client, 'action_resources')
|
||||
@patch.object(context, 'extract_context_from_environ')
|
||||
def test_start_action(self, mock_context, mock_action):
|
||||
mock_context.return_value = self.context
|
||||
mock_action.return_value = (FakeResponse(202), None)
|
||||
|
||||
t_pod, b_pods = self._prepare_pod()
|
||||
t_server_id = self._prepare_server(b_pods[0])
|
||||
self.controller.server_id = t_server_id
|
||||
|
||||
body = {'os-start': ''}
|
||||
res = self.controller.post(**body)
|
||||
mock_action.assert_called_once_with(
|
||||
'server', self.context, 'start', t_server_id)
|
||||
self.assertEqual(202, res.status)
|
||||
|
||||
@patch.object(pecan, 'response', new=FakeResponse)
|
||||
@patch.object(client.Client, 'action_resources')
|
||||
@patch.object(context, 'extract_context_from_environ')
|
||||
def test_stop_action(self, mock_context, mock_action):
|
||||
mock_context.return_value = self.context
|
||||
mock_action.return_value = (FakeResponse(202), None)
|
||||
|
||||
t_pod, b_pods = self._prepare_pod()
|
||||
t_server_id = self._prepare_server(b_pods[0])
|
||||
self.controller.server_id = t_server_id
|
||||
|
||||
body = {'os-stop': ''}
|
||||
res = self.controller.post(**body)
|
||||
mock_action.assert_called_once_with(
|
||||
'server', self.context, 'stop', t_server_id)
|
||||
self.assertEqual(202, res.status)
|
||||
|
||||
def tearDown(self):
|
||||
core.ModelBase.metadata.drop_all(core.get_engine())
|
Loading…
x
Reference in New Issue
Block a user