Merge "Add tainted field to nodes"

This commit is contained in:
Zuul 2019-10-24 04:01:36 +00:00 committed by Gerrit Code Review
commit ac49353899
20 changed files with 215 additions and 20 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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

View File

@ -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 = [

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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.")

View File

@ -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,
}

View File

@ -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):

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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):