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):
|
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):
|
def __init__(self, pod_name=None):
|
||||||
self.auth_url = cfg.CONF.client.auth_url
|
self.auth_url = cfg.CONF.client.auth_url
|
||||||
self.resource_service_map = {}
|
self.resource_service_map = {}
|
||||||
@ -458,6 +517,8 @@ class Client(object):
|
|||||||
server_volume -> create_server_volume
|
server_volume -> create_server_volume
|
||||||
-> server_id, volume_id, device=None
|
-> server_id, volume_id, device=None
|
||||||
-> none
|
-> none
|
||||||
|
server -> start -> server_id -> none
|
||||||
|
server -> stop -> server_id -> none
|
||||||
--------------------------
|
--------------------------
|
||||||
:return: None
|
:return: None
|
||||||
:raises: EndpointNotAvailable
|
:raises: EndpointNotAvailable
|
||||||
|
@ -202,10 +202,14 @@ class NeutronResourceHandle(ResourceHandle):
|
|||||||
'neutron', client.httpclient.endpoint_url)
|
'neutron', client.httpclient.endpoint_url)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_into_with_meta(item, resp):
|
||||||
|
return resp, item
|
||||||
|
|
||||||
|
|
||||||
class NovaResourceHandle(ResourceHandle):
|
class NovaResourceHandle(ResourceHandle):
|
||||||
service_type = cons.ST_NOVA
|
service_type = cons.ST_NOVA
|
||||||
support_resource = {'flavor': LIST,
|
support_resource = {'flavor': LIST,
|
||||||
'server': LIST | CREATE | GET,
|
'server': LIST | CREATE | GET | ACTION,
|
||||||
'aggregate': LIST | CREATE | DELETE | ACTION,
|
'aggregate': LIST | CREATE | DELETE | ACTION,
|
||||||
'server_volume': ACTION}
|
'server_volume': ACTION}
|
||||||
|
|
||||||
@ -288,6 +292,9 @@ class NovaResourceHandle(ResourceHandle):
|
|||||||
client = self._get_client(cxt)
|
client = self._get_client(cxt)
|
||||||
collection = '%ss' % resource
|
collection = '%ss' % resource
|
||||||
resource_manager = getattr(client, collection)
|
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)
|
return getattr(resource_manager, action)(*args, **kwargs)
|
||||||
except r_exceptions.ConnectTimeout:
|
except r_exceptions.ConnectTimeout:
|
||||||
self.endpoint_url = None
|
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 context as ctx
|
||||||
from tricircle.common import xrpcapi
|
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 aggregate
|
||||||
from tricircle.nova_apigw.controllers import flavor
|
from tricircle.nova_apigw.controllers import flavor
|
||||||
from tricircle.nova_apigw.controllers import image
|
from tricircle.nova_apigw.controllers import image
|
||||||
@ -95,7 +96,8 @@ class V21Controller(object):
|
|||||||
'limits': quota_sets.LimitsController,
|
'limits': quota_sets.LimitsController,
|
||||||
}
|
}
|
||||||
self.server_sub_controller = {
|
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):
|
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