senlin/senlin/policies/scaling_policy.py

284 lines
9.9 KiB
Python

# 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 oslo_config import cfg
from oslo_utils import timeutils
from senlin.common import constraints
from senlin.common import consts
from senlin.common import exception as exc
from senlin.common.i18n import _
from senlin.common import scaleutils as su
from senlin.common import schema
from senlin.common import utils
from senlin.objects import cluster_policy as cpo
from senlin.policies import base
CONF = cfg.CONF
class ScalingPolicy(base.Policy):
"""Policy for changing the size of a cluster.
This policy is expected to be enforced before the node count of a cluster
is changed.
"""
VERSION = '1.0'
VERSIONS = {
'1.0': [
{'status': consts.SUPPORTED, 'since': '2016.04'}
]
}
PRIORITY = 100
TARGET = [
('BEFORE', consts.CLUSTER_SCALE_IN),
('BEFORE', consts.CLUSTER_SCALE_OUT),
('AFTER', consts.CLUSTER_SCALE_IN),
('AFTER', consts.CLUSTER_SCALE_OUT),
]
PROFILE_TYPE = [
'ANY',
]
KEYS = (
EVENT, ADJUSTMENT,
) = (
'event', 'adjustment',
)
_SUPPORTED_EVENTS = (
CLUSTER_SCALE_IN, CLUSTER_SCALE_OUT,
) = (
consts.CLUSTER_SCALE_IN, consts.CLUSTER_SCALE_OUT,
)
_ADJUSTMENT_KEYS = (
ADJUSTMENT_TYPE, ADJUSTMENT_NUMBER, MIN_STEP, BEST_EFFORT,
COOLDOWN,
) = (
'type', 'number', 'min_step', 'best_effort',
'cooldown',
)
properties_schema = {
EVENT: schema.String(
_('Event that will trigger this policy. Must be one of '
'CLUSTER_SCALE_IN and CLUSTER_SCALE_OUT.'),
constraints=[
constraints.AllowedValues(_SUPPORTED_EVENTS),
],
required=True,
),
ADJUSTMENT: schema.Map(
_('Detailed specification for scaling adjustments.'),
schema={
ADJUSTMENT_TYPE: schema.String(
_('Type of adjustment when scaling is triggered.'),
constraints=[
constraints.AllowedValues(consts.ADJUSTMENT_TYPES),
],
default=consts.CHANGE_IN_CAPACITY,
),
ADJUSTMENT_NUMBER: schema.Number(
_('A number specifying the amount of adjustment.'),
default=1,
),
MIN_STEP: schema.Integer(
_('When adjustment type is set to "CHANGE_IN_PERCENTAGE",'
' this specifies the cluster size will be decreased by '
'at least this number of nodes.'),
default=1,
),
BEST_EFFORT: schema.Boolean(
_('Whether do best effort scaling when new size of '
'cluster will break the size limitation'),
default=False,
),
COOLDOWN: schema.Integer(
_('Number of seconds to hold the cluster for cool-down '
'before allowing cluster to be resized again.'),
default=0,
),
}
),
}
def __init__(self, name, spec, **kwargs):
"""Initialize a scaling policy object.
:param name: Name for the policy object.
:param spec: A dictionary containing the detailed specification for
the policy.
:param dict kwargs: Other optional parameters for policy object
creation.
:return: An object of `ScalingPolicy`.
"""
super(ScalingPolicy, self).__init__(name, spec, **kwargs)
self.singleton = False
self.event = self.properties[self.EVENT]
adjustment = self.properties[self.ADJUSTMENT]
self.adjustment_type = adjustment[self.ADJUSTMENT_TYPE]
self.adjustment_number = adjustment[self.ADJUSTMENT_NUMBER]
self.adjustment_min_step = adjustment[self.MIN_STEP]
self.best_effort = adjustment[self.BEST_EFFORT]
self.cooldown = adjustment[self.COOLDOWN]
def validate(self, context, validate_props=False):
super(ScalingPolicy, self).validate(context, validate_props)
if self.adjustment_number <= 0:
msg = _("the 'number' for 'adjustment' must be > 0")
raise exc.InvalidSpec(message=msg)
if self.adjustment_min_step < 0:
msg = _("the 'min_step' for 'adjustment' must be >= 0")
raise exc.InvalidSpec(message=msg)
if self.cooldown < 0:
msg = _("the 'cooldown' for 'adjustment' must be >= 0")
raise exc.InvalidSpec(message=msg)
def _calculate_adjustment_count(self, current_size):
"""Calculate adjustment count based on current_size.
:param current_size: The current size of the target cluster.
:return: The number of nodes to add or to remove.
"""
if self.adjustment_type == consts.EXACT_CAPACITY:
if self.event == consts.CLUSTER_SCALE_IN:
count = current_size - self.adjustment_number
else:
count = self.adjustment_number - current_size
elif self.adjustment_type == consts.CHANGE_IN_CAPACITY:
count = self.adjustment_number
else: # consts.CHANGE_IN_PERCENTAGE:
count = int((self.adjustment_number * current_size) / 100.0)
if count < self.adjustment_min_step:
count = self.adjustment_min_step
return count
def pre_op(self, cluster_id, action):
"""The hook function that is executed before the action.
The checking result is stored in the ``data`` property of the action
object rather than returned directly from the function.
:param cluster_id: The ID of the target cluster.
:param action: Action instance against which the policy is being
checked.
:return: None.
"""
# check cooldown
last_op = action.inputs.get('last_op', None)
if last_op and not timeutils.is_older_than(last_op, self.cooldown):
action.data.update({
'status': base.CHECK_ERROR,
'reason': _('Policy %s cooldown is still '
'in progress.') % self.id
})
action.store(action.context)
return
# Use action input if count is provided
count_value = action.inputs.get('count', None)
cluster = action.entity
current = len(cluster.nodes)
if count_value is None:
# count not specified, calculate it
count_value = self._calculate_adjustment_count(current)
# Count must be positive value
success, count = utils.get_positive_int(count_value)
if not success:
action.data.update({
'status': base.CHECK_ERROR,
'reason': _("Invalid count (%(c)s) for action '%(a)s'."
) % {'c': count_value, 'a': action.action}
})
action.store(action.context)
return
# Check size constraints
max_size = cluster.max_size
if max_size == -1:
max_size = cfg.CONF.max_nodes_per_cluster
if action.action == consts.CLUSTER_SCALE_IN:
if self.best_effort:
count = min(count, current - cluster.min_size)
result = su.check_size_params(cluster, current - count,
strict=not self.best_effort)
else:
if self.best_effort:
count = min(count, max_size - current)
result = su.check_size_params(cluster, current + count,
strict=not self.best_effort)
if result:
# failed validation
pd = {
'status': base.CHECK_ERROR,
'reason': result
}
else:
# passed validation
pd = {
'status': base.CHECK_OK,
'reason': _('Scaling request validated.'),
}
if action.action == consts.CLUSTER_SCALE_IN:
pd['deletion'] = {'count': count}
else:
pd['creation'] = {'count': count}
action.data.update(pd)
action.store(action.context)
return
def post_op(self, cluster_id, action):
# update last_op for next cooldown check
ts = timeutils.utcnow(True)
cpo.ClusterPolicy.update(action.context, cluster_id,
self.id, {'last_op': ts})
def need_check(self, target, action):
# check if target + action matches policy targets
if not super(ScalingPolicy, self).need_check(target, action):
return False
if target == 'BEFORE':
# Scaling policy BEFORE check should only be triggered if the
# incoming action matches the specific policy event.
# E.g. for scale-out policy the BEFORE check to select nodes for
# termination should only run for scale-out actions.
return self.event == action.action
else:
# Scaling policy AFTER check to reset cooldown timer should be
# triggered for all supported policy events (both scale-in and
# scale-out). E.g. a scale-out policy should reset cooldown timer
# whenever scale-out or scale-in action completes.
return action.action in list(self._SUPPORTED_EVENTS)