senlin/senlin/policies/deletion_policy.py

274 lines
9.2 KiB
Python
Executable File

# 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.
"""
Policy for deleting node(s) from a cluster.
NOTE: For full documentation about how the deletion policy works, check:
https://docs.openstack.org/senlin/latest/contributor/policies/deletion_v1.html
"""
from oslo_log import log as logging
from senlin.common import constraints
from senlin.common import consts
from senlin.common.i18n import _
from senlin.common import scaleutils as su
from senlin.common import schema
from senlin.policies import base
LOG = logging.getLogger(__name__)
class DeletionPolicy(base.Policy):
"""Policy for choosing victim node(s) from a cluster for deletion.
This policy is enforced when nodes are to be removed from a cluster.
It will yield an ordered list of candidates for deletion based on user
specified criteria.
"""
VERSION = '1.1'
VERSIONS = {
'1.0': [
{'status': consts.SUPPORTED, 'since': '2016.04'}
],
'1.1': [
{'status': consts.SUPPORTED, 'since': '2018.01'}
],
}
PRIORITY = 400
KEYS = (
CRITERIA, DESTROY_AFTER_DELETION, GRACE_PERIOD,
REDUCE_DESIRED_CAPACITY, HOOKS, TYPE, PARAMS, QUEUE, URL, TIMEOUT
) = (
'criteria', 'destroy_after_deletion', 'grace_period',
'reduce_desired_capacity', 'hooks', 'type', 'params', 'queue', 'url',
'timeout'
)
CRITERIA_VALUES = (
OLDEST_FIRST, OLDEST_PROFILE_FIRST, YOUNGEST_FIRST, RANDOM,
) = (
'OLDEST_FIRST', 'OLDEST_PROFILE_FIRST', 'YOUNGEST_FIRST', 'RANDOM',
)
HOOK_VALUES = (
ZAQAR, WEBHOOK
) = (
'zaqar', 'webhook',
)
TARGET = [
('BEFORE', consts.CLUSTER_SCALE_IN),
('BEFORE', consts.CLUSTER_DEL_NODES),
('BEFORE', consts.CLUSTER_RESIZE),
('BEFORE', consts.NODE_DELETE),
]
PROFILE_TYPE = [
'ANY'
]
properties_schema = {
CRITERIA: schema.String(
_('Criteria used in selecting candidates for deletion'),
default=RANDOM,
constraints=[
constraints.AllowedValues(CRITERIA_VALUES),
]
),
DESTROY_AFTER_DELETION: schema.Boolean(
_('Whether a node should be completely destroyed after '
'deletion. Default to True'),
default=True,
),
GRACE_PERIOD: schema.Integer(
_('Number of seconds before real deletion happens.'),
default=0,
),
REDUCE_DESIRED_CAPACITY: schema.Boolean(
_('Whether the desired capacity of the cluster should be '
'reduced along the deletion. Default to True.'),
default=True,
),
HOOKS: schema.Map(
_("Lifecycle hook properties"),
schema={
TYPE: schema.String(
_("Type of lifecycle hook"),
default=ZAQAR,
constraints=[
constraints.AllowedValues(HOOK_VALUES),
]
),
PARAMS: schema.Map(
schema={
QUEUE: schema.String(
_("Zaqar queue to receive lifecycle hook message"),
default="",
),
URL: schema.String(
_("Url sink to which to send lifecycle hook "
"message"),
default="",
),
},
default={}
),
TIMEOUT: schema.Integer(
_('Number of seconds before actual deletion happens.'),
default=0,
),
},
default={}
)
}
def __init__(self, name, spec, **kwargs):
super(DeletionPolicy, self).__init__(name, spec, **kwargs)
self.criteria = self.properties[self.CRITERIA]
self.grace_period = self.properties[self.GRACE_PERIOD]
self.destroy_after_deletion = self.properties[
self.DESTROY_AFTER_DELETION]
self.reduce_desired_capacity = self.properties[
self.REDUCE_DESIRED_CAPACITY]
self.hooks = self.properties[self.HOOKS]
def _victims_by_regions(self, cluster, regions):
victims = []
for region in sorted(regions.keys()):
count = regions[region]
nodes = cluster.nodes_by_region(region)
if self.criteria == self.RANDOM:
candidates = su.nodes_by_random(nodes, count)
elif self.criteria == self.OLDEST_PROFILE_FIRST:
candidates = su.nodes_by_profile_age(nodes, count)
elif self.criteria == self.OLDEST_FIRST:
candidates = su.nodes_by_age(nodes, count, True)
else:
candidates = su.nodes_by_age(nodes, count, False)
victims.extend(candidates)
return victims
def _victims_by_zones(self, cluster, zones):
victims = []
for zone in sorted(zones.keys()):
count = zones[zone]
nodes = cluster.nodes_by_zone(zone)
if self.criteria == self.RANDOM:
candidates = su.nodes_by_random(nodes, count)
elif self.criteria == self.OLDEST_PROFILE_FIRST:
candidates = su.nodes_by_profile_age(nodes, count)
elif self.criteria == self.OLDEST_FIRST:
candidates = su.nodes_by_age(nodes, count, True)
else:
candidates = su.nodes_by_age(nodes, count, False)
victims.extend(candidates)
return victims
def _update_action(self, action, victims):
pd = action.data.get('deletion', {})
pd['count'] = len(victims)
pd['candidates'] = victims
pd['destroy_after_deletion'] = self.destroy_after_deletion
pd['grace_period'] = self.grace_period
pd['reduce_desired_capacity'] = self.reduce_desired_capacity
action.data.update({
'status': base.CHECK_OK,
'reason': _('Candidates generated'),
'deletion': pd
})
action.store(action.context)
def pre_op(self, cluster_id, action):
"""Choose victims that can be deleted.
:param cluster_id: ID of the cluster to be handled.
:param action: The action object that triggered this policy.
"""
victims = action.inputs.get('candidates', [])
if len(victims) > 0:
self._update_action(action, victims)
return
if action.action == consts.NODE_DELETE:
self._update_action(action, [action.entity.id])
return
cluster = action.entity
regions = None
zones = None
hooks_data = self.hooks
action.data.update({'status': base.CHECK_OK,
'reason': _('lifecycle hook parameters saved'),
'hooks': hooks_data})
action.store(action.context)
deletion = action.data.get('deletion', {})
if deletion:
# there are policy decisions
count = deletion['count']
regions = deletion.get('regions', None)
zones = deletion.get('zones', None)
# No policy decision, check action itself: SCALE_IN
elif action.action == consts.CLUSTER_SCALE_IN:
count = action.inputs.get('count', 1)
# No policy decision, check action itself: RESIZE
else:
current = len(cluster.nodes)
res, reason = su.parse_resize_params(action, cluster, current)
if res == base.CHECK_ERROR:
action.data['status'] = base.CHECK_ERROR
action.data['reason'] = reason
LOG.error(reason)
return
if 'deletion' not in action.data:
return
count = action.data['deletion']['count']
# Cross-region
if regions:
victims = self._victims_by_regions(cluster, regions)
self._update_action(action, victims)
return
# Cross-AZ
if zones:
victims = self._victims_by_zones(cluster, zones)
self._update_action(action, victims)
return
if count > len(cluster.nodes):
count = len(cluster.nodes)
if self.criteria == self.RANDOM:
victims = su.nodes_by_random(cluster.nodes, count)
elif self.criteria == self.OLDEST_PROFILE_FIRST:
victims = su.nodes_by_profile_age(cluster.nodes, count)
elif self.criteria == self.OLDEST_FIRST:
victims = su.nodes_by_age(cluster.nodes, count, True)
else:
victims = su.nodes_by_age(cluster.nodes, count, False)
self._update_action(action, victims)
return