Implement server action API(part 1)

In this patch start and stop action are implemented.

Change-Id: I8381b75199b6307897257a74675eeeda91ba66b5
This commit is contained in:
zhiyuan_cai 2016-04-14 15:50:38 +08:00
parent 08e868823f
commit fbaaf20324
5 changed files with 340 additions and 2 deletions

View File

@ -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

View File

@ -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

View 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)

View File

@ -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):

View 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())