274 lines
9.2 KiB
Python
Executable File
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
|