senlin/senlin/common/scaleutils.py

329 lines
12 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.
"""
Utilities for scaling actions and related policies.
"""
import math
import random
from oslo_config import cfg
from oslo_log import log as logging
from senlin.common import consts
from senlin.common.i18n import _
LOG = logging.getLogger(__name__)
def calculate_desired(current, adj_type, number, min_step):
"""Calculate desired capacity based on the type and number values.
:param current: Current capacity of the cluster.
:param adj_type: Type of adjustment.
:param number: Number for the corresponding adjustment type.
:param min_step: Minimum number of nodes to create/delete.
:returns: A number representing the desired capacity.
"""
if adj_type == consts.EXACT_CAPACITY:
desired = number
elif adj_type == consts.CHANGE_IN_CAPACITY:
desired = current + number
else: # consts.CHANGE_IN_PERCENTAGE:
delta = (number * current) / 100.0
if delta > 0.0:
rounded = int(math.ceil(delta) if math.fabs(delta) < 1.0
else math.floor(delta))
else:
rounded = int(math.floor(delta) if math.fabs(delta) < 1.0
else math.ceil(delta))
if min_step is not None and min_step > abs(rounded):
adjust = min_step if rounded > 0 else -min_step
desired = current + adjust
else:
desired = current + rounded
return desired
def truncate_desired(cluster, desired, min_size, max_size):
"""Do truncation of desired capacity for non-strict cases.
:param cluster: The target cluster.
:param desired: The expected capacity of the cluster.
:param min_size: The NEW minimum capacity set for the cluster.
:param max_size: The NEW maximum capacity set for the cluster.
"""
if min_size is not None and desired < min_size:
desired = min_size
LOG.debug("Truncating shrinkage to specified min_size (%s).",
desired)
if min_size is None and desired < cluster.min_size:
desired = cluster.min_size
LOG.debug("Truncating shrinkage to cluster's min_size (%s).",
desired)
if max_size is not None and max_size > 0 and desired > max_size:
desired = max_size
LOG.debug("Truncating growth to specified max_size (%s).",
desired)
if (max_size is None and desired > cluster.max_size and
cluster.max_size > 0):
desired = cluster.max_size
LOG.debug("Truncating growth to cluster's max_size (%s).",
desired)
return desired
def check_size_params(cluster=None, desired=None, min_size=None, max_size=None,
strict=False):
"""Validate provided arguments against cluster properties.
Sanity Checking 1: the desired, min_size, max_size parameters must
form a reasonable relationship among themselves,
if specified.
Sanity Checking 2: the desired_capacity must be within the existing
range of the cluster, if new range is not provided.
:param cluster: The cluster object if provided.
:param desired: The desired capacity for an operation if provided.
:param min_size: The new min_size property for the cluster, if provided.
:param max_size: The new max_size property for the cluster, if provided.
:param strict: Whether we are doing a strict checking.
:return: A string of error message if failed checking or None if passed
the checking.
"""
max_nodes_per_cluster = cfg.CONF.max_nodes_per_cluster
if desired is not None:
# recalculate/validate desired based on strict setting
if desired > max_nodes_per_cluster:
v = {'d': desired, 'm': max_nodes_per_cluster}
return _("The target capacity (%(d)s) is greater than the "
"maximum number of nodes allowed per cluster "
"(%(m)s).") % v
if (min_size is not None and desired < min_size):
v = {'d': desired, 'm': min_size}
return _("The target capacity (%(d)s) is less than "
"the specified min_size (%(m)s).") % v
if (min_size is None and cluster is not None and
desired < cluster.min_size and strict):
v = {'d': desired, 'm': cluster.min_size}
return _("The target capacity (%(d)s) is less than "
"the cluster's min_size (%(m)s).") % v
if (max_size is not None and desired > max_size and
max_size >= 0):
v = {'d': desired, 'm': max_size}
return _("The target capacity (%(d)s) is greater "
"than the specified max_size (%(m)s).") % v
if (max_size is None and cluster is not None and
desired > cluster.max_size and
cluster.max_size >= 0 and strict):
v = {'d': desired, 'm': cluster.max_size}
return _("The target capacity (%(d)s) is greater "
"than the cluster's max_size (%(m)s).") % v
if min_size is not None:
if max_size is not None and max_size >= 0 and min_size > max_size:
v = {'n': min_size, 'm': max_size}
return _("The specified min_size (%(n)s) is greater than the "
"specified max_size (%(m)s).") % v
if (max_size is None and cluster is not None and
cluster.max_size >= 0 and min_size > cluster.max_size):
v = {'n': min_size, 'm': cluster.max_size}
return _("The specified min_size (%(n)s) is greater than the "
"current max_size (%(m)s) of the cluster.") % v
if (desired is None and cluster is not None and
min_size > cluster.desired_capacity and strict):
v = {'n': min_size, 'd': cluster.desired_capacity}
return _("The specified min_size (%(n)s) is greater than the "
"current desired_capacity (%(d)s) of the cluster.") % v
if max_size is not None:
if max_size > max_nodes_per_cluster:
v = {'m': max_size, 'mc': max_nodes_per_cluster}
return _("The specified max_size (%(m)s) is greater than the "
"maximum number of nodes allowed per cluster "
"(%(mc)s).") % v
if (min_size is None and cluster is not None and
max_size >= 0 and max_size < cluster.min_size):
v = {'m': max_size, 'n': cluster.min_size}
return _("The specified max_size (%(m)s) is less than the "
"current min_size (%(n)s) of the cluster.") % v
if (desired is None and cluster is not None and
max_size >= 0 and max_size < cluster.desired_capacity and
strict):
v = {'m': max_size, 'd': cluster.desired_capacity}
return _("The specified max_size (%(m)s) is less than the "
"current desired_capacity (%(d)s) of the cluster.") % v
return None
def parse_resize_params(action, cluster, current=None):
"""Parse the parameters of CLUSTER_RESIZE action.
:param action: The current action which contains some inputs for parsing.
:param cluster: The target cluster to operate.
:param current: The current capacity of the cluster.
:returns: A tuple containing a flag and a message. In the case of a
success, the flag should be action.RES_OK and the message can be
ignored. The action.data will contain a dict indicating the
operation and parameters for further processing. In the case of
a failure, the flag should be action.RES_ERROR and the message
will contain a string message indicating the reason of failure.
"""
adj_type = action.inputs.get(consts.ADJUSTMENT_TYPE, None)
number = action.inputs.get(consts.ADJUSTMENT_NUMBER, None)
min_size = action.inputs.get(consts.ADJUSTMENT_MIN_SIZE, None)
max_size = action.inputs.get(consts.ADJUSTMENT_MAX_SIZE, None)
min_step = action.inputs.get(consts.ADJUSTMENT_MIN_STEP, None)
strict = action.inputs.get(consts.ADJUSTMENT_STRICT, False)
current = current or cluster.desired_capacity
if adj_type is not None:
# number must be not None according to previous tests
desired = calculate_desired(current, adj_type, number, min_step)
else:
desired = current
# truncate adjustment if permitted (strict==False)
if strict is False:
desired = truncate_desired(cluster, desired, min_size, max_size)
# check provided params against current properties
# desired is checked when strict is True
result = check_size_params(cluster, desired, min_size, max_size, strict)
if result:
return action.RES_ERROR, result
# save sanitized properties
count = current - desired
if count > 0:
action.data.update({
'deletion': {
'count': count,
}
})
else:
action.data.update({
'creation': {
'count': abs(count),
}
})
return action.RES_OK, ''
def filter_error_nodes(nodes):
"""Filter out ERROR nodes from the given node list.
:param nodes: candidate nodes for filter.
:return: a tuple containing the chosen nodes' IDs and the undecided
(good) nodes.
"""
good = []
bad = []
not_created = []
for n in nodes:
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)
else:
good.append(n)
bad.extend(not_created)
return bad, good
def nodes_by_random(nodes, count):
"""Select nodes based on random number.
:param nodes: list of candidate nodes.
:param count: maximum number of nodes for selection.
:return: a list of IDs for victim nodes.
"""
selected, candidates = filter_error_nodes(nodes)
if count <= len(selected):
return selected[:count]
count -= len(selected)
random.seed()
i = count
while i > 0:
rand = random.randrange(len(candidates))
selected.append(candidates[rand].id)
candidates.remove(candidates[rand])
i = i - 1
return selected
def nodes_by_age(nodes, count, old_first):
"""Select nodes based on node creation time.
:param nodes: list of candidate nodes.
:param count: maximum number of nodes for selection.
:param old_first: whether old nodes should appear before young ones.
:return: a list of IDs for victim nodes.
"""
selected, candidates = filter_error_nodes(nodes)
if count <= len(selected):
return selected[:count]
count -= len(selected)
sorted_list = sorted(candidates, key=lambda r: r.created_at)
for i in range(count):
if old_first:
selected.append(sorted_list[i].id)
else: # YOUNGEST_FIRST
selected.append(sorted_list[-1 - i].id)
return selected
def nodes_by_profile_age(nodes, count):
"""Select nodes based on node profile creation time.
Note that old nodes will come before young ones.
:param nodes: list of candidate nodes.
:param count: maximum number of nodes for selection.
:return: a list of IDs for victim nodes.
"""
selected, candidates = filter_error_nodes(nodes)
if count <= len(selected):
return selected[:count]
count -= len(selected)
sorted_list = sorted(candidates, key=lambda n: n.profile_created_at)
for i in range(count):
selected.append(sorted_list[i].id)
return selected