Port Scale Down Functionality into Workflow
This patch ports the logic of the ScaleDownManager into an action and an associated workflow to expose it. Partial-Bug: #1626736 Co-Authored-By: Dougal Matthews <dougal@redhat.com> Change-Id: Ia31a03c8f0a99dd92b18ffbf76d689b037ef8a78
This commit is contained in:
parent
ab7c7df0d9
commit
28edd93db6
|
@ -78,6 +78,7 @@ mistral.actions =
|
||||||
tripleo.plan.delete = tripleo_common.actions.plan:DeletePlanAction
|
tripleo.plan.delete = tripleo_common.actions.plan:DeletePlanAction
|
||||||
tripleo.plan.list = tripleo_common.actions.plan:ListPlansAction
|
tripleo.plan.list = tripleo_common.actions.plan:ListPlansAction
|
||||||
tripleo.role.list = tripleo_common.actions.plan:ListRolesAction
|
tripleo.role.list = tripleo_common.actions.plan:ListRolesAction
|
||||||
|
tripleo.scale.delete_node = tripleo_common.actions.scale:ScaleDownAction
|
||||||
tripleo.templates.process = tripleo_common.actions.templates:ProcessTemplatesAction
|
tripleo.templates.process = tripleo_common.actions.templates:ProcessTemplatesAction
|
||||||
tripleo.templates.upload_default = tripleo_common.actions.templates:UploadTemplatesAction
|
tripleo.templates.upload_default = tripleo_common.actions.templates:UploadTemplatesAction
|
||||||
tripleo.validations.get_pubkey = tripleo_common.actions.validations:GetPubkeyAction
|
tripleo.validations.get_pubkey = tripleo_common.actions.validations:GetPubkeyAction
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
# Copyright 2016 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
import collections
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from mistral.workflow import utils as mistral_workflow_utils
|
||||||
|
|
||||||
|
|
||||||
|
from tripleo_common.actions import parameters as parameters_actions
|
||||||
|
from tripleo_common.actions import templates
|
||||||
|
from tripleo_common import constants
|
||||||
|
from tripleo_common import update
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_group_resources_after_delete(groupname, res_to_delete, resources):
|
||||||
|
group = next(res for res in resources if
|
||||||
|
res.resource_name == groupname and
|
||||||
|
res.resource_type == constants.RESOURCE_GROUP_TYPE)
|
||||||
|
members = []
|
||||||
|
for res in resources:
|
||||||
|
stack_name, stack_id = next(
|
||||||
|
x['href'] for x in res.links if
|
||||||
|
x['rel'] == 'stack').rsplit('/', 2)[1:]
|
||||||
|
# desired new count of nodes after delete operation should be
|
||||||
|
# count of all existing nodes in ResourceGroup which are not
|
||||||
|
# in set of nodes being deleted. Also nodes in any delete state
|
||||||
|
# from a previous failed update operation are not included in
|
||||||
|
# overall count (if such nodes exist)
|
||||||
|
if (stack_id == group.physical_resource_id and
|
||||||
|
res not in res_to_delete and
|
||||||
|
not res.resource_status.startswith('DELETE')):
|
||||||
|
|
||||||
|
members.append(res)
|
||||||
|
|
||||||
|
return members
|
||||||
|
|
||||||
|
|
||||||
|
class ScaleDownAction(templates.ProcessTemplatesAction):
|
||||||
|
"""Deletes overcloud nodes
|
||||||
|
|
||||||
|
Before calling this method, ensure you have updated the plan
|
||||||
|
with any templates or environment files as needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, timeout, nodes=[],
|
||||||
|
container=constants.DEFAULT_CONTAINER_NAME):
|
||||||
|
self.nodes = nodes
|
||||||
|
self.timeout_mins = timeout
|
||||||
|
super(ScaleDownAction, self).__init__(container)
|
||||||
|
|
||||||
|
def _update_stack(self, parameters={},
|
||||||
|
timeout_mins=constants.STACK_TIMEOUT_DEFAULT):
|
||||||
|
# TODO(rbrady): migrate _update_stack to it's own action and update
|
||||||
|
# the workflow for scale down
|
||||||
|
|
||||||
|
# update the plan parameters with the scaled down parameters
|
||||||
|
update_params_action = parameters_actions.UpdateParametersAction(
|
||||||
|
parameters, self.container)
|
||||||
|
updated_plan = update_params_action.run()
|
||||||
|
if isinstance(updated_plan, mistral_workflow_utils.Result):
|
||||||
|
return updated_plan
|
||||||
|
|
||||||
|
processed_data = super(ScaleDownAction, self).run()
|
||||||
|
if isinstance(processed_data, mistral_workflow_utils.Result):
|
||||||
|
return processed_data
|
||||||
|
|
||||||
|
update.add_breakpoints_cleanup_into_env(processed_data['environment'])
|
||||||
|
|
||||||
|
fields = processed_data.copy()
|
||||||
|
fields['timeout_mins'] = timeout_mins
|
||||||
|
fields['existing'] = True
|
||||||
|
# TODO(rbrady): test against live overcloud to see if it is required
|
||||||
|
# to pass 'parameters' here to the stack update, or will storing the
|
||||||
|
# updated values in the plan and send them merged into the environment
|
||||||
|
# work as well
|
||||||
|
fields['parameters'] = parameters
|
||||||
|
|
||||||
|
LOG.debug('stack update params: %s', fields)
|
||||||
|
self._get_orchestration_client().stacks.update(self.container,
|
||||||
|
**fields)
|
||||||
|
|
||||||
|
def _get_removal_params_from_heat(self, resources_by_role, resources):
|
||||||
|
stack_params = {}
|
||||||
|
for role, role_resources in resources_by_role.items():
|
||||||
|
param_name = "{0}Count".format(role)
|
||||||
|
|
||||||
|
# get real count of nodes for each role. *Count stack parameters
|
||||||
|
# can not be used because stack parameters return parameters
|
||||||
|
# passed by user no matter if previous update operation succeeded
|
||||||
|
# or not
|
||||||
|
group_members = get_group_resources_after_delete(
|
||||||
|
role, role_resources, resources)
|
||||||
|
stack_params[param_name] = str(len(group_members))
|
||||||
|
|
||||||
|
# add instance resource names into removal_policies
|
||||||
|
# so heat knows which instances should be removed
|
||||||
|
removal_param = "{0}RemovalPolicies".format(role)
|
||||||
|
stack_params[removal_param] = [{
|
||||||
|
'resource_list': [r.resource_name for r in role_resources]
|
||||||
|
}]
|
||||||
|
|
||||||
|
return stack_params
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
heatclient = self._get_orchestration_client()
|
||||||
|
resources = heatclient.resources.list(self.container, nested_depth=5)
|
||||||
|
resources_by_role = collections.defaultdict(list)
|
||||||
|
instance_list = list(self.nodes)
|
||||||
|
for res in resources:
|
||||||
|
try:
|
||||||
|
instance_list.remove(res.physical_resource_id)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
stack_name, stack_id = next(
|
||||||
|
x['href'] for x in res.links if
|
||||||
|
x['rel'] == 'stack').rsplit('/', 2)[1:]
|
||||||
|
# get resource to remove from resource group (it's parent resource
|
||||||
|
# of nova server)
|
||||||
|
role_resource = next(x for x in resources if
|
||||||
|
x.physical_resource_id == stack_id)
|
||||||
|
# get the role name which is parent resource name in Heat
|
||||||
|
role = role_resource.parent_resource
|
||||||
|
resources_by_role[role].append(role_resource)
|
||||||
|
|
||||||
|
resources_by_role = dict(resources_by_role)
|
||||||
|
|
||||||
|
if instance_list:
|
||||||
|
raise ValueError(
|
||||||
|
"Couldn't find following instances in stack %s: %s" %
|
||||||
|
(self.container, ','.join(instance_list)))
|
||||||
|
|
||||||
|
# decrease count for each role (or resource group) and set removal
|
||||||
|
# policy for each resource group
|
||||||
|
stack_params = self._get_removal_params_from_heat(
|
||||||
|
resources_by_role, resources)
|
||||||
|
|
||||||
|
self._update_stack(parameters=stack_params)
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Copyright 2016 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
import collections
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from swiftclient import exceptions as swiftexceptions
|
||||||
|
|
||||||
|
from tripleo_common.actions import scale
|
||||||
|
from tripleo_common import constants
|
||||||
|
from tripleo_common.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
def mock_stack():
|
||||||
|
stack = mock.Mock()
|
||||||
|
stack.name = 'My Stack'
|
||||||
|
stack.parameters = {'ComputeCount': '2'}
|
||||||
|
stack.to_dict.return_value = {
|
||||||
|
'uuid': 5,
|
||||||
|
'name': 'My Stack',
|
||||||
|
'parameters': stack.parameters,
|
||||||
|
}
|
||||||
|
return stack
|
||||||
|
|
||||||
|
|
||||||
|
class ScaleDownActionTest(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ScaleDownActionTest, self).setUp()
|
||||||
|
self.image = collections.namedtuple('image', ['id'])
|
||||||
|
|
||||||
|
@mock.patch('tripleo_common.actions.base.TripleOAction.'
|
||||||
|
'_get_orchestration_client')
|
||||||
|
@mock.patch('heatclient.common.template_utils.'
|
||||||
|
'process_multiple_environments_and_files')
|
||||||
|
@mock.patch('heatclient.common.template_utils.get_template_contents')
|
||||||
|
@mock.patch('tripleo_common.actions.base.TripleOAction.'
|
||||||
|
'_get_workflow_client')
|
||||||
|
@mock.patch('tripleo_common.actions.base.TripleOAction._get_object_client')
|
||||||
|
@mock.patch('mistral.context.ctx')
|
||||||
|
def test_run(self, mock_ctx, mock_get_object_client,
|
||||||
|
mock_get_workflow_client, mock_get_template_contents,
|
||||||
|
mock_env_files, mock_get_heat_client):
|
||||||
|
|
||||||
|
mock_env_files.return_value = ({}, {})
|
||||||
|
heatclient = mock.MagicMock()
|
||||||
|
heatclient.resources.list.return_value = [
|
||||||
|
mock.MagicMock(
|
||||||
|
links=[{'rel': 'stack',
|
||||||
|
'href': 'http://192.0.2.1:8004/v1/'
|
||||||
|
'a959ac7d6a4a475daf2428df315c41ef/'
|
||||||
|
'stacks/overcloud/123'}],
|
||||||
|
logical_resource_id='logical_id',
|
||||||
|
physical_resource_id='resource_id',
|
||||||
|
resource_type='OS::Heat::ResourceGroup',
|
||||||
|
resource_name='Compute'
|
||||||
|
),
|
||||||
|
mock.MagicMock(
|
||||||
|
links=[{'rel': 'stack',
|
||||||
|
'href': 'http://192.0.2.1:8004/v1/'
|
||||||
|
'a959ac7d6a4a475daf2428df315c41ef/'
|
||||||
|
'stacks/overcloud/124'}],
|
||||||
|
logical_resource_id='node0',
|
||||||
|
physical_resource_id='123',
|
||||||
|
resource_type='OS::TripleO::Compute',
|
||||||
|
parent_resource='Compute',
|
||||||
|
resource_name='node0',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
heatclient.stacks.get.return_value = mock_stack()
|
||||||
|
mock_get_heat_client.return_value = heatclient
|
||||||
|
|
||||||
|
mock_ctx.return_value = mock.MagicMock()
|
||||||
|
swift = mock.MagicMock(url="http://test.com")
|
||||||
|
swift.get_object.side_effect = swiftexceptions.ClientException(
|
||||||
|
'atest2')
|
||||||
|
mock_get_object_client.return_value = swift
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'resource_registry': {
|
||||||
|
'resources': {'*': {'*': {'UpdateDeployment': {'hooks': []}}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_mistral = mock.MagicMock()
|
||||||
|
mock_env = mock.MagicMock()
|
||||||
|
mock_env.variables = {
|
||||||
|
'temp_environment': 'temp_environment',
|
||||||
|
'template': 'template',
|
||||||
|
'environments': [{u'path': u'environments/test.yaml'}],
|
||||||
|
}
|
||||||
|
mock_mistral.environments.get.return_value = mock_env
|
||||||
|
mock_get_workflow_client.return_value = mock_mistral
|
||||||
|
|
||||||
|
mock_get_template_contents.return_value = ({}, {
|
||||||
|
'heat_template_version': '2016-04-30'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Test
|
||||||
|
action = scale.ScaleDownAction(
|
||||||
|
constants.STACK_TIMEOUT_DEFAULT, ['resource_id'], 'stack')
|
||||||
|
action.run()
|
||||||
|
|
||||||
|
heatclient.stacks.update.assert_called_once_with(
|
||||||
|
'stack',
|
||||||
|
stack_name='stack',
|
||||||
|
template={'heat_template_version': '2016-04-30'},
|
||||||
|
environment=env,
|
||||||
|
existing=True,
|
||||||
|
files={},
|
||||||
|
timeout_mins=240,
|
||||||
|
parameters={
|
||||||
|
'ComputeCount': '0',
|
||||||
|
'ComputeRemovalPolicies': [
|
||||||
|
{'resource_list': ['node0']}
|
||||||
|
]
|
||||||
|
})
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
version: '2.0'
|
||||||
|
name: tripleo.scale.v1
|
||||||
|
description: TripleO Overcloud Deployment Workflows v1
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
|
||||||
|
delete_node:
|
||||||
|
description: deletes given overcloud nodes and updates the stack
|
||||||
|
|
||||||
|
input:
|
||||||
|
- container
|
||||||
|
- nodes
|
||||||
|
- timeout: 240
|
||||||
|
- queue_name: tripleo
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
delete_node:
|
||||||
|
action: tripleo.scale.delete_node nodes=<% $.nodes %> timeout=<% $.timeout %> container=<% $.container %>
|
||||||
|
on-success: send_message
|
||||||
|
on-error: set_delete_node_failed
|
||||||
|
|
||||||
|
set_delete_node_failed:
|
||||||
|
on-success: send_message
|
||||||
|
publish:
|
||||||
|
status: FAILED
|
||||||
|
message: <% task(delete_node).result %>
|
||||||
|
|
||||||
|
send_message:
|
||||||
|
action: zaqar.queue_post
|
||||||
|
retry: count=5 delay=1
|
||||||
|
input:
|
||||||
|
queue_name: <% $.queue_name %>
|
||||||
|
messages:
|
||||||
|
body:
|
||||||
|
type: tripleo.scale.v1.delete_node
|
||||||
|
payload:
|
||||||
|
status: <% $.get('status', 'SUCCESS') %>
|
||||||
|
message: <% $.get('message', '') %>
|
||||||
|
execution: <% execution() %>
|
Loading…
Reference in New Issue