Merge "Add tainted field to nodes"
This commit is contained in:
commit
ac49353899
|
@ -67,6 +67,7 @@ Response Parameters
|
|||
- role: role
|
||||
- status: node_status
|
||||
- status_reason: status_reason
|
||||
- tainted: tainted
|
||||
- updated_at: updated_at
|
||||
- user: user
|
||||
|
||||
|
@ -142,6 +143,7 @@ Response Parameters
|
|||
- role: role
|
||||
- status: node_status
|
||||
- status_reason: status_reason
|
||||
- tainted: tainted
|
||||
- updated_at: updated_at
|
||||
- user: user
|
||||
|
||||
|
@ -220,6 +222,7 @@ Response Parameters
|
|||
- role: role
|
||||
- status: node_status
|
||||
- status_reason: status_reason
|
||||
- tainted: tainted
|
||||
- updated_at: updated_at
|
||||
- user: user
|
||||
|
||||
|
@ -346,6 +349,7 @@ Response Parameters
|
|||
- role: role
|
||||
- status: node_status
|
||||
- status_reason: status_reason
|
||||
- tainted: tainted
|
||||
- updated_at: updated_at
|
||||
- user: user
|
||||
|
||||
|
@ -390,6 +394,7 @@ Request Parameters
|
|||
- profile_id: profile_identity
|
||||
- role: role_req
|
||||
- metadata: metadata_req
|
||||
- tainted: tainted_req
|
||||
|
||||
Request Example
|
||||
---------------
|
||||
|
@ -422,6 +427,7 @@ Response Parameters
|
|||
- role: role
|
||||
- status: node_status
|
||||
- status_reason: status_reason
|
||||
- tainted: tainted
|
||||
- updated_at: updated_at
|
||||
- user: user
|
||||
|
||||
|
|
|
@ -1425,6 +1425,24 @@ status_reason:
|
|||
The string representation of the reason why the object has transited to
|
||||
its current status.
|
||||
|
||||
tainted:
|
||||
type: bool
|
||||
in: body
|
||||
required: True
|
||||
description: |
|
||||
A boolean indicating whether a node is considered tainted. Tainted nodes
|
||||
are selected first during scale-in operations. This field is only
|
||||
returned starting with API microversion 1.13 or greater.
|
||||
|
||||
tainted_req:
|
||||
type: bool
|
||||
in: body
|
||||
required: False
|
||||
description: |
|
||||
A boolean indicating whether a node is considered tainted. Tainted nodes
|
||||
are selected first during scale-in operations. This parameter is only
|
||||
accepted starting with API microversion 1.13 or greater.
|
||||
|
||||
timeout:
|
||||
type: integer
|
||||
in: body
|
||||
|
|
|
@ -55,7 +55,8 @@ The valid values for the "``criteria`` property include:
|
|||
.. NOTE::
|
||||
|
||||
There is an implicit rule (criteria) when electing victim nodes. Senlin
|
||||
engine always rank those nodes which are not in ACTIVE state before others.
|
||||
engine always rank those nodes which are not in ACTIVE state or which are
|
||||
marked as tainted before others.
|
||||
|
||||
There are more several actions that can trigger a deletion policy. Some of
|
||||
them may already carry a list of candidates to remove, e.g.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Add tainted field to nodes. A node with tainted set to True will be
|
||||
selected first for scale-in operations.
|
|
@ -122,3 +122,7 @@ it can be used by both users and developers.
|
|||
- Added ``action_update`` API. This API enables users to update the status of
|
||||
an action (only CANCELLED is supported). An action that spawns dependent
|
||||
actions will attempt to cancel all dependent actions.
|
||||
|
||||
1.13
|
||||
----
|
||||
- Added ``tainted`` to responses returned by node APIs.
|
||||
|
|
|
@ -16,6 +16,7 @@ Node endpoint for Senlin v1 REST API.
|
|||
from webob import exc
|
||||
|
||||
from senlin.api.common import util
|
||||
from senlin.api.common import version_request as vr
|
||||
from senlin.api.common import wsgi
|
||||
from senlin.common import consts
|
||||
from senlin.common.i18n import _
|
||||
|
@ -32,6 +33,15 @@ class NodeController(wsgi.Controller):
|
|||
'check', 'recover'
|
||||
)
|
||||
|
||||
def _remove_tainted(self, req, obj):
|
||||
if req.version_request > vr.APIVersionRequest("1.12"):
|
||||
return obj
|
||||
|
||||
if 'tainted' in obj:
|
||||
obj.pop('tainted')
|
||||
|
||||
return obj
|
||||
|
||||
@util.policy_enforce
|
||||
def index(self, req):
|
||||
whitelist = {
|
||||
|
@ -55,6 +65,8 @@ class NodeController(wsgi.Controller):
|
|||
|
||||
obj = util.parse_request('NodeListRequest', req, params)
|
||||
nodes = self.rpc_client.call(req.context, 'node_list', obj)
|
||||
|
||||
nodes = [self._remove_tainted(req, n) for n in nodes]
|
||||
return {'nodes': nodes}
|
||||
|
||||
@util.policy_enforce
|
||||
|
@ -63,6 +75,9 @@ class NodeController(wsgi.Controller):
|
|||
obj = util.parse_request('NodeCreateRequest', req, body, 'node')
|
||||
node = self.rpc_client.call(req.context, 'node_create',
|
||||
obj.node)
|
||||
|
||||
node = self._remove_tainted(req, node)
|
||||
|
||||
action_id = node.pop('action')
|
||||
result = {
|
||||
'node': node,
|
||||
|
@ -76,6 +91,8 @@ class NodeController(wsgi.Controller):
|
|||
"""Adopt a node for management."""
|
||||
obj = util.parse_request('NodeAdoptRequest', req, body)
|
||||
node = self.rpc_client.call(req.context, 'node_adopt', obj)
|
||||
|
||||
node = self._remove_tainted(req, node)
|
||||
return {'node': node}
|
||||
|
||||
@wsgi.Controller.api_version('1.7')
|
||||
|
@ -97,6 +114,8 @@ class NodeController(wsgi.Controller):
|
|||
|
||||
obj = util.parse_request('NodeGetRequest', req, params)
|
||||
node = self.rpc_client.call(req.context, 'node_get', obj)
|
||||
|
||||
node = self._remove_tainted(req, node)
|
||||
return {'node': node}
|
||||
|
||||
@util.policy_enforce
|
||||
|
@ -111,6 +130,8 @@ class NodeController(wsgi.Controller):
|
|||
obj = util.parse_request('NodeUpdateRequest', req, params)
|
||||
node = self.rpc_client.call(req.context, 'node_update', obj)
|
||||
|
||||
node = self._remove_tainted(req, node)
|
||||
|
||||
action_id = node.pop('action')
|
||||
result = {
|
||||
'node': node,
|
||||
|
|
|
@ -23,7 +23,7 @@ class VersionController(object):
|
|||
# This includes any semantic changes which may not affect the input or
|
||||
# output formats or even originate in the API code layer.
|
||||
_MIN_API_VERSION = "1.0"
|
||||
_MAX_API_VERSION = "1.12"
|
||||
_MAX_API_VERSION = "1.13"
|
||||
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
|
|
@ -138,11 +138,11 @@ CLUSTER_SORT_KEYS = [
|
|||
NODE_ATTRS = (
|
||||
NODE_INDEX, NODE_NAME, NODE_PROFILE_ID, NODE_CLUSTER_ID,
|
||||
NODE_INIT_AT, NODE_CREATED_AT, NODE_UPDATED_AT,
|
||||
NODE_STATUS, NODE_ROLE, NODE_METADATA,
|
||||
NODE_STATUS, NODE_ROLE, NODE_METADATA, NODE_TAINTED,
|
||||
) = (
|
||||
'index', 'name', 'profile_id', 'cluster_id',
|
||||
'init_at', 'created_at', 'updated_at',
|
||||
'status', 'role', 'metadata',
|
||||
'status', 'role', 'metadata', 'tainted',
|
||||
)
|
||||
|
||||
NODE_SORT_KEYS = [
|
||||
|
|
|
@ -249,7 +249,8 @@ def filter_error_nodes(nodes):
|
|||
bad = []
|
||||
not_created = []
|
||||
for n in nodes:
|
||||
if n.status == consts.NS_ERROR or n.status == consts.NS_WARNING:
|
||||
if (n.status == consts.NS_ERROR or n.status == consts.NS_WARNING or
|
||||
n.tainted):
|
||||
bad.append(n.id)
|
||||
elif n.created_at is None:
|
||||
not_created.append(n.id)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# 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 sqlalchemy import MetaData, Boolean, Table, Column
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
node = Table('node', meta, autoload=True)
|
||||
node_tainted = Column('tainted', Boolean)
|
||||
node_tainted.create(node)
|
|
@ -117,6 +117,7 @@ class Node(BASE, TimestampMixin, models.ModelBase):
|
|||
|
||||
init_at = Column(types.TZAwareDateTime)
|
||||
|
||||
tainted = Column(Boolean)
|
||||
status = Column(String(255))
|
||||
status_reason = Column(Text)
|
||||
meta_data = Column(types.Dict)
|
||||
|
|
|
@ -63,6 +63,7 @@ class Node(object):
|
|||
self.data = kwargs.get('data', {})
|
||||
self.metadata = kwargs.get('metadata', {})
|
||||
self.dependents = kwargs.get('dependents', {})
|
||||
self.tainted = False
|
||||
self.rt = {}
|
||||
|
||||
if context is not None:
|
||||
|
@ -111,6 +112,7 @@ class Node(object):
|
|||
'meta_data': self.metadata,
|
||||
'data': self.data,
|
||||
'dependents': self.dependents,
|
||||
'tainted': self.tainted,
|
||||
}
|
||||
|
||||
if self.id:
|
||||
|
@ -148,6 +150,7 @@ class Node(object):
|
|||
'data': obj.data,
|
||||
'metadata': obj.metadata,
|
||||
'dependents': obj.dependents,
|
||||
'tainted': obj.tainted,
|
||||
}
|
||||
|
||||
return cls(obj.name, obj.profile_id, obj.cluster_id,
|
||||
|
@ -268,7 +271,7 @@ class Node(object):
|
|||
return False
|
||||
|
||||
props = dict([(k, v) for k, v in params.items()
|
||||
if k in ('name', 'role', 'metadata')])
|
||||
if k in ('name', 'role', 'metadata', 'tainted')])
|
||||
if new_profile_id:
|
||||
props['profile_id'] = new_profile_id
|
||||
self.rt['profile'] = pb.Profile.load(context,
|
||||
|
|
|
@ -1756,6 +1756,9 @@ class EngineService(service.Service):
|
|||
if req.obj_attr_is_set('metadata'):
|
||||
if req.metadata != node.metadata:
|
||||
inputs['metadata'] = req.metadata
|
||||
if req.obj_attr_is_set('tainted'):
|
||||
if req.tainted != node.tainted:
|
||||
inputs['tainted'] = req.tainted
|
||||
|
||||
if not inputs:
|
||||
msg = _("No property needs an update.")
|
||||
|
|
|
@ -46,6 +46,7 @@ class Node(base.SenlinObject, base.VersionedObjectDictCompat):
|
|||
'dependents': fields.JsonField(nullable=True),
|
||||
'profile_name': fields.StringField(nullable=True),
|
||||
'profile_created_at': fields.StringField(nullable=True),
|
||||
'tainted': fields.BooleanField(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -61,6 +62,9 @@ class Node(base.SenlinObject, base.VersionedObjectDictCompat):
|
|||
elif field == 'profile_created_at':
|
||||
p = db_obj['profile']
|
||||
obj['profile_created_at'] = p.created_at if p else None
|
||||
elif field == 'tainted':
|
||||
p = db_obj[field]
|
||||
obj[field] = p if p else False
|
||||
else:
|
||||
obj[field] = db_obj[field]
|
||||
|
||||
|
@ -181,4 +185,5 @@ class Node(base.SenlinObject, base.VersionedObjectDictCompat):
|
|||
'metadata': self.metadata,
|
||||
'dependents': self.dependents,
|
||||
'profile_name': self.profile_name,
|
||||
'tainted': self.tainted,
|
||||
}
|
||||
|
|
|
@ -66,14 +66,28 @@ class NodeGetRequest(base.SenlinObject):
|
|||
@base.SenlinObjectRegistry.register
|
||||
class NodeUpdateRequest(base.SenlinObject):
|
||||
|
||||
VERSION = '1.1'
|
||||
VERSION_MAP = {
|
||||
'1.13': '1.1'
|
||||
}
|
||||
|
||||
fields = {
|
||||
'identity': fields.StringField(),
|
||||
'metadata': fields.JsonField(nullable=True),
|
||||
'name': fields.NameField(nullable=True),
|
||||
'profile_id': fields.StringField(nullable=True),
|
||||
'role': fields.StringField(nullable=True)
|
||||
'role': fields.StringField(nullable=True),
|
||||
'tainted': fields.BooleanField(nullable=True)
|
||||
}
|
||||
|
||||
def obj_make_compatible(self, primitive, target_version):
|
||||
super(NodeUpdateRequest, self).obj_make_compatible(
|
||||
primitive, target_version)
|
||||
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||
if target_version < (1, 13):
|
||||
if 'tainted' in primitive['senlin_object.data']:
|
||||
del primitive['senlin_object.data']['tainted']
|
||||
|
||||
|
||||
@base.SenlinObjectRegistry.register
|
||||
class NodeDeleteRequest(base.SenlinObject):
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import mock
|
||||
import six
|
||||
from webob import exc
|
||||
|
@ -76,6 +77,90 @@ class NodeControllerTest(shared.ControllerTest, base.SenlinTestCase):
|
|||
mock_call.assert_called_once_with(
|
||||
req.context, 'node_list', obj)
|
||||
|
||||
@mock.patch.object(util, 'parse_request')
|
||||
@mock.patch.object(rpc_client.EngineClient, 'call')
|
||||
def test_node_index_without_tainted(self, mock_call, mock_parse,
|
||||
mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'index', True)
|
||||
req = self._get('/nodes', version='1.12')
|
||||
|
||||
engine_resp = [
|
||||
{
|
||||
u'id': u'aaaa-bbbb-cccc',
|
||||
u'name': u'node-1',
|
||||
u'cluster_id': None,
|
||||
u'physical_id': None,
|
||||
u'profile_id': u'pppp-rrrr-oooo-ffff',
|
||||
u'profile_name': u'my_stack_profile',
|
||||
u'index': 1,
|
||||
u'role': None,
|
||||
u'init_time': u'2015-01-23T13:06:00Z',
|
||||
u'created_time': u'2015-01-23T13:07:22Z',
|
||||
u'updated_time': None,
|
||||
u'status': u'ACTIVE',
|
||||
u'status_reason': u'Node successfully created',
|
||||
u'data': {},
|
||||
u'metadata': {},
|
||||
u'tainted': False,
|
||||
}
|
||||
]
|
||||
|
||||
obj = mock.Mock()
|
||||
mock_parse.return_value = obj
|
||||
mock_call.return_value = copy.deepcopy(engine_resp)
|
||||
|
||||
result = self.controller.index(req)
|
||||
|
||||
# list call for version 1.12 should have tainted field removed
|
||||
# remove tainted field from expected response
|
||||
engine_resp[0].pop('tainted')
|
||||
|
||||
self.assertEqual(engine_resp, result['nodes'])
|
||||
mock_parse.assert_called_once_with(
|
||||
'NodeListRequest', req, {'project_safe': True})
|
||||
mock_call.assert_called_once_with(
|
||||
req.context, 'node_list', obj)
|
||||
|
||||
@mock.patch.object(util, 'parse_request')
|
||||
@mock.patch.object(rpc_client.EngineClient, 'call')
|
||||
def test_node_index_with_tainted(self, mock_call, mock_parse,
|
||||
mock_enforce):
|
||||
self._mock_enforce_setup(mock_enforce, 'index', True)
|
||||
req = self._get('/nodes', version='1.13')
|
||||
|
||||
engine_resp = [
|
||||
{
|
||||
u'id': u'aaaa-bbbb-cccc',
|
||||
u'name': u'node-1',
|
||||
u'cluster_id': None,
|
||||
u'physical_id': None,
|
||||
u'profile_id': u'pppp-rrrr-oooo-ffff',
|
||||
u'profile_name': u'my_stack_profile',
|
||||
u'index': 1,
|
||||
u'role': None,
|
||||
u'init_time': u'2015-01-23T13:06:00Z',
|
||||
u'created_time': u'2015-01-23T13:07:22Z',
|
||||
u'updated_time': None,
|
||||
u'status': u'ACTIVE',
|
||||
u'status_reason': u'Node successfully created',
|
||||
u'data': {},
|
||||
u'metadata': {},
|
||||
u'tainted': False,
|
||||
}
|
||||
]
|
||||
|
||||
obj = mock.Mock()
|
||||
mock_parse.return_value = obj
|
||||
mock_call.return_value = copy.deepcopy(engine_resp)
|
||||
|
||||
result = self.controller.index(req)
|
||||
|
||||
self.assertEqual(engine_resp, result['nodes'])
|
||||
mock_parse.assert_called_once_with(
|
||||
'NodeListRequest', req, {'project_safe': True})
|
||||
mock_call.assert_called_once_with(
|
||||
req.context, 'node_list', obj)
|
||||
|
||||
@mock.patch.object(util, 'parse_request')
|
||||
@mock.patch.object(rpc_client.EngineClient, 'call')
|
||||
def test_node_index_whitelists_params(self, mock_call,
|
||||
|
|
|
@ -107,6 +107,7 @@ def create_node(ctx, cluster, profile, **kwargs):
|
|||
'meta_data': jsonutils.loads('{"foo": "123"}'),
|
||||
'data': jsonutils.loads('{"key1": "value1"}'),
|
||||
'dependents': {},
|
||||
'tainted': False,
|
||||
}
|
||||
values.update(kwargs)
|
||||
return db_api.node_create(ctx, values)
|
||||
|
|
|
@ -129,6 +129,7 @@ class TestNode(base.SenlinTestCase):
|
|||
'metadata': node.metadata,
|
||||
'dependents': node.dependents,
|
||||
'profile_name': node.profile_name,
|
||||
'tainted': False,
|
||||
}
|
||||
|
||||
result = no.Node.get(self.ctx, node.id)
|
||||
|
|
|
@ -90,9 +90,9 @@ class TestBatchPolicy(base.SenlinTestCase):
|
|||
self.assertIn(node3.id, nodes[1])
|
||||
|
||||
def test_pick_nodes_with_error_nodes(self):
|
||||
node1 = mock.Mock(id='1', status='ACTIVE')
|
||||
node2 = mock.Mock(id='2', status='ACTIVE')
|
||||
node3 = mock.Mock(id='3', status='ERROR')
|
||||
node1 = mock.Mock(id='1', status='ACTIVE', tainted=False)
|
||||
node2 = mock.Mock(id='2', status='ACTIVE', tainted=False)
|
||||
node3 = mock.Mock(id='3', status='ERROR', tainted=False)
|
||||
nodes = [node1, node2, node3]
|
||||
|
||||
policy = bp.BatchPolicy('test-batch', self.spec)
|
||||
|
|
|
@ -195,20 +195,24 @@ class ScaleUtilsTest(base.SenlinTestCase):
|
|||
|
||||
def test_filter_error_nodes(self):
|
||||
nodes = [
|
||||
mock.Mock(id='N1', status='ACTIVE'),
|
||||
mock.Mock(id='N2', status='ACTIVE'),
|
||||
mock.Mock(id='N3', status='ERROR'),
|
||||
mock.Mock(id='N4', status='ACTIVE'),
|
||||
mock.Mock(id='N5', status='WARNING'),
|
||||
mock.Mock(id='N6', status='ERROR'),
|
||||
mock.Mock(id='N7', created_at=None)
|
||||
mock.Mock(id='N1', status='ACTIVE', tainted=None),
|
||||
mock.Mock(id='N2', tainted=None),
|
||||
mock.Mock(id='N3', status='ACTIVE', tainted=None),
|
||||
mock.Mock(id='N4', status='ERROR'),
|
||||
mock.Mock(id='N5', status='ACTIVE', tainted=None),
|
||||
mock.Mock(id='N6', status='WARNING'),
|
||||
mock.Mock(id='N7', tainted=True),
|
||||
mock.Mock(id='N8', status='ERROR'),
|
||||
mock.Mock(id='N9', created_at=None),
|
||||
mock.Mock(id='N10', tainted=False),
|
||||
]
|
||||
res = su.filter_error_nodes(nodes)
|
||||
self.assertIn('N3', res[0])
|
||||
self.assertIn('N5', res[0])
|
||||
self.assertIn('N4', res[0])
|
||||
self.assertIn('N6', res[0])
|
||||
self.assertIn('N7', res[0])
|
||||
self.assertEqual(3, len(res[1]))
|
||||
self.assertIn('N8', res[0])
|
||||
self.assertIn('N9', res[0])
|
||||
self.assertEqual(5, len(res[1]))
|
||||
|
||||
@mock.patch.object(su, 'filter_error_nodes')
|
||||
def test_nodes_by_random(self, mock_filter):
|
||||
|
|
Loading…
Reference in New Issue