API layer support for cluster-collect

This patch adds API layer support to cluster-collect.

Change-Id: I9ce475eb88b3abe8e5684735ada7206c650b1d27
This commit is contained in:
tengqm
2016-06-14 22:35:59 -04:00
parent 2d225f5139
commit 65b26229e5
10 changed files with 175 additions and 13 deletions

View File

@@ -669,12 +669,50 @@ Response Parameters
- location: location
Collect Attributes Across a Cluster
===================================
.. rest_method:: GET /v1/clusters/{cluster_id}/attrs/{path}
- min_version: 1.2
Aggregate an attribute value across all nodes in a cluster.
Normal response codes: 200
Error response codes:
- badRequest (400)
- unauthorized (401)
- forbidden (403)
- notFound (404)
- serviceUnavailable (503)
Request Parameters
------------------
.. rest_parameters:: parameters.yaml
- OpenStack-API-Version: microversion
- cluster_id: cluster_id_url
- path: cluster_attrs_path
Response Parameters
-------------------
.. rest_parameters:: parameters.yaml
- cluster_attributes: cluster_attributes
- id: node_id
- value: cluster_attr_value
Check a Cluster's Health Status
===============================
.. rest_method:: POST /v1/clusters/{cluster_id}/actions
CHeck the health status of all nodes in a cluster.
Check the health status of all nodes in a cluster.
Normal response codes: 202

View File

@@ -33,6 +33,13 @@ action_id_url:
description: |
The name or short-ID or UUID that identifies an action object.
cluster_attrs_path:
type: string
in: path
required: True
description: |
The Json path of an attribute to be aggregated across a cluster.
cluster_id_url:
type: string
in: path
@@ -473,6 +480,21 @@ cluster:
description: |
The structured definition of a cluster object.
cluster_attr_value:
type: object
in: body
description: |
The attribute value on a specific node. The value could be of any data
type that is valid for the attribute.
cluster_attributes:
type: list
in: body
required: True
description: |
A list of dictionaries each containing the node ID and the corresponding
attribute value.
cluster_data:
type: object
in: body

View File

@@ -13,6 +13,7 @@
"clusters:get": "",
"clusters:action": "",
"clusters:update": "",
"clusters:collect": "",
"profiles:index": "",
"profiles:create": "",
"profiles:get": "",

View File

@@ -0,0 +1,3 @@
---
features:
- A new ``cluster_collect`` API is added.

View File

@@ -21,3 +21,8 @@ it can be used by both users and developers.
where the ``<version>`` is any valid API version supported. If such a
header is not provided, the API behaves as if a version request of v1.0
is received.
1.2
---
Added ``cluster_collect`` API.

View File

@@ -362,6 +362,15 @@ class ClusterController(wsgi.Controller):
res.update(location)
return res
@wsgi.Controller.api_version('1.2')
@util.policy_enforce
def collect(self, req, cluster_id, path):
"""Aggregate attribute values across a cluster."""
if path.strip() == '':
raise exc.HTTPBadRequest(_("Required path attribute is missing."))
return self.rpc_client.cluster_collect(req.context, cluster_id, path)
@util.policy_enforce
def delete(self, req, cluster_id):
res = self.rpc_client.cluster_delete(req.context, cluster_id,

View File

@@ -152,6 +152,10 @@ class API(wsgi.Router):
action="action",
conditions={'method': 'POST'},
success=202)
sub_mapper.connect("cluster_collect",
"/clusters/{cluster_id}/attrs/{path}",
action="collect",
conditions={'method': 'GET'})
sub_mapper.connect("cluster_delete",
"/clusters/{cluster_id}",
action="delete",

View File

@@ -30,7 +30,7 @@ from senlin.api.common import version_request as vr
# The minimum and maximum versions of the API supported, where the default api
# version request is defined to be the minimum version supported.
_MIN_API_VERSION = "1.0"
_MAX_API_VERSION = "1.0"
_MAX_API_VERSION = "1.2"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@@ -1777,3 +1777,75 @@ class ClusterControllerTest(shared.ControllerTest, base.SenlinTestCase):
self.assertEqual(403, resp.status_int)
self.assertIn('403 Forbidden', six.text_type(resp))
def test_cluster_collect(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'collect', True)
cid = 'aaaa-bbbb-cccc'
path = 'foo.bar'
req = self._get('/clusters/%(cid)s/attrs/%(path)s' %
{'cid': cid, 'path': path}, version='1.2')
engine_response = {
'cluster_attributes': [{'key': 'value'}],
}
mock_call = self.patchobject(rpc_client.EngineClient, 'call',
return_value=engine_response)
resp = self.controller.collect(req, cluster_id=cid, path=path)
self.assertEqual(engine_response, resp)
mock_call.assert_called_once_with(
req.context,
('cluster_collect', {'identity': cid, 'path': path,
'project_safe': True}),
version='1.1')
def test_cluster_collect_version_mismatch(self, mock_enforce):
# NOTE: we skip the mock_enforce setup below because api version check
# comes before the policy enforcement and the check fails in
# this test case.
# self._mock_enforce_setup(mock_enforce, 'collect', True)
cid = 'aaaa-bbbb-cccc'
path = 'foo.bar'
req = self._get('/clusters/%(cid)s/attrs/%(path)s' %
{'cid': cid, 'path': path}, version='1.1')
mock_call = self.patchobject(rpc_client.EngineClient, 'call')
ex = self.assertRaises(senlin_exc.MethodVersionNotFound,
self.controller.collect,
req, cluster_id=cid, path=path)
self.assertEqual(0, mock_call.call_count)
self.assertEqual('API version 1.1 is not supported on this method.',
six.text_type(ex))
def test_cluster_collect_path_not_provided(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'collect', True)
cid = 'aaaa-bbbb-cccc'
path = ' '
req = self._get('/clusters/%(cid)s/attrs/%(path)s' %
{'cid': cid, 'path': path}, version='1.2')
mock_call = self.patchobject(rpc_client.EngineClient, 'call')
ex = self.assertRaises(exc.HTTPBadRequest,
self.controller.collect,
req, cluster_id=cid, path=path)
self.assertEqual(0, mock_call.call_count)
self.assertEqual('Required path attribute is missing.',
six.text_type(ex))
def test_cluster_collect_denied_policy(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'collect', False)
cid = 'aaaa-bbbb-cccc'
path = 'foo.bar'
req = self._get('/clusters/%(cid)s/attrs/%(path)s' %
{'cid': cid, 'path': path}, version='1.2')
mock_call = self.patchobject(rpc_client.EngineClient, 'call')
resp = shared.request_with_middleware(fault.FaultWrapper,
self.controller.collect,
req, cluster_id=cid, path=path)
self.assertEqual(403, resp.status_int)
self.assertIn('403 Forbidden', six.text_type(resp))
self.assertEqual(0, mock_call.call_count)

View File

@@ -17,6 +17,7 @@ from oslo_log import log
from oslo_messaging._drivers import common as rpc_common
from oslo_utils import encodeutils
from senlin.api.common import version_request as vr
from senlin.api.common import wsgi
from senlin.common import consts
from senlin.tests.unit.common import utils
@@ -64,7 +65,7 @@ class ControllerTest(object):
'wsgi.url_scheme': 'http',
}
def _simple_request(self, path, params=None, method='GET'):
def _simple_request(self, path, params=None, method='GET', version=None):
environ = self._environ(path)
environ['REQUEST_METHOD'] = method
@@ -75,33 +76,40 @@ class ControllerTest(object):
req = wsgi.Request(environ)
req.context = utils.dummy_context('api_test_user', self.project)
self.context = req.context
ver = version if version else wsgi.DEFAULT_API_VERSION
req.version_request = vr.APIVersionRequest(ver)
return req
def _get(self, path, params=None):
return self._simple_request(path, params=params)
def _get(self, path, params=None, version=None):
return self._simple_request(path, params=params, version=version)
def _delete(self, path):
def _delete(self, path, version=None):
return self._simple_request(path, method='DELETE')
def _data_request(self, path, data, content_type='application/json',
method='POST'):
method='POST', version=None):
environ = self._environ(path)
environ['REQUEST_METHOD'] = method
req = wsgi.Request(environ)
req.context = utils.dummy_context('api_test_user', self.project)
self.context = req.context
ver = version if version else wsgi.DEFAULT_API_VERSION
req.version_request = vr.APIVersionRequest(ver)
req.body = encodeutils.safe_encode(data)
return req
def _post(self, path, data, content_type='application/json'):
return self._data_request(path, data, content_type)
def _post(self, path, data, content_type='application/json', version=None):
return self._data_request(path, data, content_type, version=version)
def _put(self, path, data, content_type='application/json'):
return self._data_request(path, data, content_type, method='PUT')
def _put(self, path, data, content_type='application/json', version=None):
return self._data_request(path, data, content_type, method='PUT',
version=version)
def _patch(self, path, data, content_type='application/json'):
return self._data_request(path, data, content_type, method='PATCH')
def _patch(self, path, data, content_type='application/json',
version=None):
return self._data_request(path, data, content_type, method='PATCH',
version=version)
def tearDown(self):
# Common tearDown to assert that policy enforcement happens for all