API layer support for cluster-collect
This patch adds API layer support to cluster-collect. Change-Id: I9ce475eb88b3abe8e5684735ada7206c650b1d27
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"clusters:get": "",
|
||||
"clusters:action": "",
|
||||
"clusters:update": "",
|
||||
"clusters:collect": "",
|
||||
"profiles:index": "",
|
||||
"profiles:create": "",
|
||||
"profiles:get": "",
|
||||
|
||||
3
releasenotes/notes/cluster-collect-90e460c7bfede347.yaml
Normal file
3
releasenotes/notes/cluster-collect-90e460c7bfede347.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- A new ``cluster_collect`` API is added.
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user