Implement volume actions

The purpose of this patch set is to provide the end users with
the following functionalities:
1. Extend the size of the specified volume.
2. Set attachment metadata of the specified volume.
3. Update state of the specified volume.
4. Set image metadata of the specified volume.
5. Unset image metadata of the specified volume.
6. Show image metadata of the specified volume.

Change-Id: Ie9e4ca15a412c89a3c44f1b8526e6597eddf762c
This commit is contained in:
XiongQiu 2016-06-15 10:16:23 +08:00
parent 122d2b1866
commit 7b6a6af766
4 changed files with 524 additions and 3 deletions

View File

@ -18,6 +18,7 @@ import pecan
import oslo_log.log as logging
from tricircle.cinder_apigw.controllers import volume
from tricircle.cinder_apigw.controllers import volume_actions
from tricircle.cinder_apigw.controllers import volume_metadata
from tricircle.cinder_apigw.controllers import volume_type
@ -73,6 +74,7 @@ class V2Controller(object):
self.volumes_sub_controller = {
'metadata': volume_metadata.VolumeMetaDataController,
'action': volume_actions.VolumeActionController,
}
@pecan.expose()

View File

@ -0,0 +1,234 @@
# Copyright 2016 OpenStack Foundation.
# 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 _
from tricircle.common.i18n import _LE
from tricircle.common import utils
import tricircle.db.api as db_api
LOG = logging.getLogger(__name__)
class VolumeActionController(rest.RestController):
def __init__(self, project_id, volume_id):
self.project_id = project_id
self.volume_id = volume_id
self.clients = {constants.TOP: t_client.Client()}
self.handle_map = {
'os-attach': self._attach,
'os-extend': self._extend,
'os-reset_status': self._reset_status,
'os-set_image_metadata': self._set_image_metadata,
'os-unset_image_metadata': self._unset_image_metadata,
'os-show_image_metadata': self._show_image_metadata
}
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 _action(self, context, pod_name, action, info=None, **kwargs):
"""Perform a volume "action".
:param pod_name: the bottom pod name.
:param action: volume action name.
:param info: action parameters body.
"""
body = {action: info}
url = '/volumes/%s/action' % self.volume_id
api = self._get_client(pod_name).get_native_client('volume', context)
return api.client.post(url, body=body)
def _attach(self, context, pod_name, kw):
"""Add attachment metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
mountpoint = None
if 'mountpoint' in kw['os-attach']:
mountpoint = kw['os-attach']['mountpoint']
body = {'mountpoint': mountpoint}
instance_uuid = None
if 'instance_uuid' in kw['os-attach']:
instance_uuid = kw['os-attach']['instance_uuid']
host_name = None
if 'host_name' in kw['os-attach']:
host_name = kw['os-attach']['host_name']
except (KeyError, ValueError, TypeError):
msg = _('The server could not comply with the request since '
'it is either malformed or otherwise incorrect.')
return utils.format_cinder_error(400, msg)
if instance_uuid is not None:
body.update({'instance_uuid': instance_uuid})
if host_name is not None:
body.update({'host_name': host_name})
return self._action(context, pod_name, 'os-attach', body)
def _extend(self, context, pod_name, kw):
"""Extend the size of the specified volume.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
new_size = int(kw['os-extend']['new_size'])
except (KeyError, ValueError, TypeError):
msg = _("New volume size must be specified as an integer.")
return utils.format_cinder_error(400, msg)
return self._action(context, pod_name, 'os-extend',
{'new_size': new_size})
def _reset_status(self, context, pod_name, kw):
"""Update the provided volume with the provided state.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
status = None
if 'status' in kw['os-reset_status']:
status = kw['os-reset_status']['status']
attach_status = None
if 'attach_status' in kw['os-reset_status']:
attach_status = kw['os-reset_status']['attach_status']
migration_status = None
if 'migration_status' in kw['os-reset_status']:
migration_status = kw['os-reset_status']['migration_status']
except (TypeError, KeyError, ValueError):
msg = _('The server has either erred or is incapable of '
'performing the requested operation.')
return utils.format_cinder_error(500, msg)
body = {'status': status} if status else {}
if attach_status:
body.update({'attach_status': attach_status})
if migration_status:
body.update({'migration_status': migration_status})
return self._action(context, pod_name, 'os-reset_status', body)
def _set_image_metadata(self, context, pod_name, kw):
"""Set a volume's image metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
metadata = kw['os-set_image_metadata']['metadata']
except (KeyError, TypeError):
msg = _("Malformed request body.")
return utils.format_cinder_error(400, msg)
return self._action(context, pod_name, 'os-set_image_metadata',
{'metadata': metadata})
def _unset_image_metadata(self, context, pod_name, kw):
"""Unset specified keys from volume's image metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
key = kw['os-unset_image_metadata']['key']
except (KeyError, TypeError):
msg = _("Malformed request body.")
return utils.format_cinder_error(400, msg)
return self._action(
context, pod_name, 'os-unset_image_metadata', {'key': key})
def _show_image_metadata(self, context, pod_name, kw):
"""Show a volume's image metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
return self._action(context, pod_name, 'os-show_image_metadata')
@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 utils.format_cinder_error(
400, _('Volume action not supported'))
volume_mappings = db_api.get_bottom_mappings_by_top_id(
context, self.volume_id, constants.RT_VOLUME)
if not volume_mappings:
return utils.format_cinder_error(
404, _('Volume %(volume_id)s could not be found.') % {
'volume_id': self.volume_id
})
pod_name = volume_mappings[0][0]['pod_name']
if action_type == 'os-attach':
instance_uuid = kw['os-attach'].get('instance_uuid')
if instance_uuid is not None:
server_mappings = db_api.get_bottom_mappings_by_top_id(
context, instance_uuid, constants.RT_SERVER)
if not server_mappings:
return utils.format_cinder_error(
404, _('Server not found'))
server_pod_name = server_mappings[0][0]['pod_name']
if server_pod_name != pod_name:
LOG.error(_LE('Server %(server)s is in pod %(server_pod)s'
'and volume %(volume)s is in pod'
'%(volume_pod)s, which '
'are not the same.'),
{'server': instance_uuid,
'server_pod': server_pod_name,
'volume': self.volume_id,
'volume_pod': pod_name})
return utils.format_cinder_error(
400, _('Server and volume not in the same pod'))
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 volume %(volume_id)s fails') % {
'action': action_type,
'volume_id': self.volume_id}
if hasattr(e, 'code'):
code = e.code
ex_message = str(e)
if ex_message:
message = ex_message
LOG.error(message)
return utils.format_cinder_error(code, message)

View File

@ -304,16 +304,16 @@ class NovaResourceHandle(ResourceHandle):
class CinderResourceHandle(ResourceHandle):
service_type = cons.ST_CINDER
support_resource = {'volume': GET | ACTION,
support_resource = {'volume': LIST | CREATE | DELETE | GET | ACTION,
'transfer': CREATE | ACTION}
def _get_client(self, cxt):
cli = c_client.Client('2',
auth_token=cxt.auth_token,
auth_url=self.auth_url,
timeout=cfg.CONF.client.cinder_timeout)
cli.set_management_url(
cli.client.set_management_url(
self.endpoint_url.replace('$(tenant_id)s', cxt.tenant))
cli.client.auth_token = cxt.auth_token
return cli
def handle_get(self, cxt, resource, resource_id):

View File

@ -0,0 +1,285 @@
# 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 cinderclient.client import HTTPClient
from oslo_utils import uuidutils
from tricircle.cinder_apigw.controllers import volume_actions as action
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
class FakeResponse(object):
def __new__(cls, code=500):
cls.status = code
cls.status_code = code
return super(FakeResponse, cls).__new__(cls)
class VolumeActionTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
self.project_id = 'test_project'
self.context.tenant = self.project_id
self.controller = action.VolumeActionController(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_pod_service(self, pod_id, service):
config_dict = {'service_id': uuidutils.generate_uuid(),
'pod_id': pod_id,
'service_type': service,
'service_url': 'fake_pod_service'}
api.create_pod_service_configuration(self.context, config_dict)
pass
def _prepare_volume(self, pod):
t_volume_id = uuidutils.generate_uuid()
b_volume_id = t_volume_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_volume_id, 'bottom_id': b_volume_id,
'pod_id': pod['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_VOLUME})
return t_volume_id
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('Volume action not supported',
res['badRequest']['message'])
self.assertEqual(400, res['badRequest']['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_action_volume_not_found(self, mock_context):
mock_context.return_value = self.context
body = {'os-extend': ''}
self.controller.volume_id = 'Fake_volume_id'
res = self.controller.post(**body)
self.assertEqual(
'Volume %(volume_id)s could not be found.' % {
'volume_id': self.controller.volume_id},
res['itemNotFound']['message'])
self.assertEqual(404, res['itemNotFound']['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@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()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
mock_action.side_effect = exceptions.HTTPForbiddenError(
msg='Volume operation forbidden')
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
# this is the message of HTTPForbiddenError exception
self.assertEqual('Volume operation forbidden',
res['forbidden']['message'])
# this is the code of HTTPForbiddenError exception
self.assertEqual(403, res['forbidden']['code'])
mock_action.side_effect = exceptions.ServiceUnavailable
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
# this is the message of ServiceUnavailable exception
self.assertEqual('The service is unavailable',
res['internalServerError']['message'])
# code is 500 by default
self.assertEqual(500, res['internalServerError']['code'])
mock_action.side_effect = Exception
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
# use default message if exception's message is empty
self.assertEqual('Action os-extend on volume %s fails' % t_volume_id,
res['internalServerError']['message'])
# code is 500 by default
self.assertEqual(500, res['internalServerError']['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_extend_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()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_attach_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()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
t_server_id = self._prepare_server(b_pods[0])
self.controller.volume_id = t_volume_id
body = {'os-attach': {
'instance_uuid': t_server_id,
'mountpoint': '/dev/vdc'
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_reset_status_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()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-reset_status": {
"status": "available",
"attach_status": "detached",
"migration_status": "migrating"
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_set_image_metadata_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()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-set_image_metadata": {
"metadata": {
"image_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
"image_name": "image",
"kernel_id": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"ramdisk_id": "somedisk"
}
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_unset_image_metadata_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()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-unset_image_metadata": {
'key': 'image_name'
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_show_image_metadata_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()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-show_image_metadata": None}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())