Adds UpdateRoles Workflow

This patch adds a workflow and associated actions to update
roles in a deployment plan.

Implements: blueprint update-roles-action

Change-Id: I42d66da37dcb07c1be112623d89769377bb23284
This commit is contained in:
Ryan Brady 2017-10-17 16:01:01 -04:00
parent fceaf440b4
commit a9e26a8d77
6 changed files with 404 additions and 3 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Adds a workflow and associated actions to update roles in a deployment
plan.

View File

@ -113,6 +113,8 @@ mistral.actions =
tripleo.plan.update_from_dir = tripleo_common.actions.plan:UpdatePlanFromDirAction
tripleo.plan.update_networks = tripleo_common.actions.plan:UpdateNetworksAction
tripleo.plan.update_plan_environment = tripleo_common.actions.plan:UpdatePlanEnvironmentAction
tripleo.plan.update_roles = tripleo_common.actions.plan:UpdateRolesAction
tripleo.plan.validate_roles = tripleo_common.actions.plan:ValidateRolesDataAction
tripleo.logging_to_swift.format_messages = tripleo_common.actions.logging_to_swift:FormatMessagesAction
tripleo.logging_to_swift.publish_ui_log_to_swift = tripleo_common.actions.logging_to_swift:PublishUILogToSwiftAction
tripleo.logging_to_swift.prepare_log_download = tripleo_common.actions.logging_to_swift:PrepareLogDownloadAction

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
from operator import itemgetter
import shutil
import tempfile
import yaml
@ -27,6 +28,7 @@ from tripleo_common.actions import base
from tripleo_common import constants
from tripleo_common import exception
from tripleo_common.utils import plan as plan_utils
from tripleo_common.utils import roles as roles_utils
from tripleo_common.utils import swift as swiftutils
from tripleo_common.utils import tarball
from tripleo_common.utils.validations import pattern_validator
@ -144,16 +146,18 @@ class ListRolesAction(base.TripleOAction):
"""
def __init__(self, container=constants.DEFAULT_CONTAINER_NAME,
role_file_name=constants.OVERCLOUD_J2_ROLES_NAME,
detail=False):
super(ListRolesAction, self).__init__()
self.container = container
self.role_file_name = role_file_name
self.detail = detail
def run(self, context):
try:
swift = self.get_object_client(context)
roles_data = yaml.safe_load(swift.get_object(
self.container, constants.OVERCLOUD_J2_ROLES_NAME)[1])
self.container, self.role_file_name)[1])
except Exception as err:
err_msg = ("Error retrieving roles data from deployment plan: %s"
% err)
@ -346,3 +350,87 @@ class UpdateNetworksAction(base.TripleOAction):
}.values()]
return actions.Result(data={'network_data': network_data_to_save})
class ValidateRolesDataAction(base.TripleOAction):
"""Validates Roles Data
Validates the format of input (verify that each role in input has the
required attributes set. see README in roles directory in t-h-t),
validates that roles in input exist in roles directory in deployment plan
"""
def __init__(self, roles, available_roles,
container=constants.DEFAULT_CONTAINER_NAME):
super(ValidateRolesDataAction, self).__init__()
self.container = container
self.roles = roles
self.available_roles = available_roles
def run(self, context):
err_msg = ""
# validate roles in input exist in roles directory in t-h-t
try:
roles_utils.check_role_exists(
[role['name'] for role in self.available_roles],
[role['name'] for role in self.roles])
except Exception as chk_err:
err_msg = str(chk_err)
# validate role yaml
for role in self.roles:
try:
roles_utils.validate_role_yaml(yaml.safe_dump([role]))
except exception.RoleMetadataError as rme:
if 'name' in role:
err_msg += "\n%s for %s" % (str(rme), role['name'])
else:
err_msg += "\n%s" % str(rme)
if err_msg:
return actions.Result(error=err_msg)
return actions.Result(data=True)
class UpdateRolesAction(base.TripleOAction):
"""Updates roles_data.yaml object in plan with given roles.
:param roles: role input data (json)
:param current_roles: data from roles_data.yaml file in plan (json)
:param replace_all: boolean value indicating if input roles should merge
with or replace data from roles_data.yaml. Defaults to False (merge)
:param container: name of the Swift container / plan name
"""
def __init__(self, roles, current_roles, replace_all=False,
container=constants.DEFAULT_CONTAINER_NAME):
super(UpdateRolesAction, self).__init__()
self.container = container
self.roles = roles
self.current_roles = current_roles
self.replace_all = replace_all
def run(self, context):
role_data_to_save = self.roles
# if replace_all flag is true, discard current roles and save input
# if replace_all flag is false, merge input into current roles
if not self.replace_all:
# merge the roles_data and the role_input into roles to be saved
role_data_to_save = [role for role in {
x['name']: x for x in
self.current_roles + self.roles
}.values()]
# ensure required primary tag exists in roles to be saved
primary = [role for role in role_data_to_save if
'tags' in role and 'primary' in role['tags']]
if len(primary) < 1:
# throw error
raise exception.RoleMetadataError("At least one role must contain"
" a 'primary' tag.")
# sort the data to have a predictable result
save_roles = sorted(role_data_to_save, key=itemgetter('name'),
reverse=True)
return actions.Result(data={'roles': save_roles})

View File

@ -14,6 +14,7 @@
# under the License.
import mock
from heatclient import exc as heatexceptions
from mistral_lib import actions
from oslo_concurrency import processutils
@ -70,6 +71,77 @@ ROLES_DATA_YAML_CONTENTS = """
- OS::TripleO::Services::Kernel
"""
SAMPLE_ROLE = """
###############################################################################
# Role: sample #
###############################################################################
- name: sample
description: |
Sample!
networks:
- InternalApi
HostnameFormatDefault: '%stackname%-sample-%index%'
ServicesDefault:
- OS::TripleO::Services::Ntp
"""
SAMPLE_ROLE_OBJ = {
'HostnameFormatDefault': '%stackname%-sample-%index%',
'ServicesDefault': ['OS::TripleO::Services::Ntp'],
'description': 'Sample!\n',
'name': 'sample',
'networks': ['InternalApi']
}
SAMPLE_ROLE_2 = """
###############################################################################
# Role: sample2 #
###############################################################################
- name: sample2
description: |
Sample2!
networks:
- InternalApi
HostnameFormatDefault: '%stackname%-sample-%index%'
ServicesDefault:
- OS::TripleO::Services::Ntp
"""
SAMPLE_ROLE_2_OBJ = {
'HostnameFormatDefault': '%stackname%-sample-%index%',
'ServicesDefault': ['OS::TripleO::Services::Ntp'],
'description': 'Sample2!\n',
'name': 'sample2',
'networks': ['InternalApi']
}
UPDATED_ROLE = """
###############################################################################
# Role: sample #
###############################################################################
- name: sample
description: |
Sample!
networks:
- InternalApi
- ExternalApi
tags:
- primary
HostnameFormatDefault: '%stackname%-sample-%index%'
ServicesDefault:
- OS::TripleO::Services::Ntp
"""
UPDATED_ROLE_OBJ = {
'HostnameFormatDefault': '%stackname%-sample-%index%',
'ServicesDefault': ['OS::TripleO::Services::Ntp'],
'description': 'Sample!\n',
'name': 'sample',
'networks': ['InternalApi', 'ExternalApi'],
'tags': ['primary']
}
class CreateContainerActionTest(base.TestCase):
@ -446,3 +518,88 @@ class UpdateNetworksActionTest(base.TestCase):
{'name': 'MyReplacementNetwork'}
]
self.assertEqual({"network_data": expected}, result.data)
class ValidateRolesDataActionTest(base.TestCase):
def setUp(self):
super(ValidateRolesDataActionTest, self).setUp()
self.container = 'overcloud'
self.ctx = mock.MagicMock()
def test_valid_roles(self):
current_roles = [SAMPLE_ROLE_OBJ]
requested_roles = [SAMPLE_ROLE_OBJ]
action = plan.ValidateRolesDataAction(requested_roles, current_roles)
result = action.run(self.ctx)
self.assertTrue(result.data)
def test_invalid_roles(self):
current_roles = [SAMPLE_ROLE_2_OBJ]
requested_roles = [SAMPLE_ROLE_OBJ, ]
action = plan.ValidateRolesDataAction(requested_roles, current_roles)
result = action.run(self.ctx)
self.assertTrue(result.error)
def test_validate_role_yaml_missing_name(self):
role = SAMPLE_ROLE_OBJ.copy()
del role['name']
current_roles = [SAMPLE_ROLE_OBJ]
requested_roles = [role, ]
action = plan.ValidateRolesDataAction(requested_roles, current_roles)
result = action.run(self.ctx)
self.assertTrue(result.error)
def test_validate_role_yaml_invalid_type(self):
role = SAMPLE_ROLE_OBJ.copy()
role['CountDefault'] = 'should not be a string'
current_roles = [SAMPLE_ROLE_OBJ]
requested_roles = [role, ]
action = plan.ValidateRolesDataAction(requested_roles, current_roles)
result = action.run(self.ctx)
self.assertTrue(result.error)
class UpdateRolesActionTest(base.TestCase):
def setUp(self):
super(UpdateRolesActionTest, self).setUp()
self.container = 'overcloud'
self.ctx = mock.MagicMock()
self.current_roles = [SAMPLE_ROLE_OBJ, SAMPLE_ROLE_2_OBJ]
def test_no_primary_roles(self):
updated_role = UPDATED_ROLE_OBJ.copy()
del updated_role['tags']
action = plan.UpdateRolesAction([updated_role],
self.current_roles,
True, self.container)
self.assertRaises(exception.RoleMetadataError, action.run, self.ctx)
@mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client')
def test_update_some_roles(self, get_obj_client_mock):
# Setup
swift = mock.MagicMock()
get_obj_client_mock.return_value = swift
action = plan.UpdateRolesAction([UPDATED_ROLE_OBJ],
self.current_roles,
False, self.container)
result = action.run(self.ctx)
self.assertEqual(result.data,
{'roles': [SAMPLE_ROLE_2_OBJ, UPDATED_ROLE_OBJ]})
@mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client')
def test_update_replace_roles(self, get_obj_client_mock):
# Setup
swift = mock.MagicMock()
get_obj_client_mock.return_value = swift
action = plan.UpdateRolesAction([UPDATED_ROLE_OBJ],
self.current_roles,
True, self.container)
result = action.run(self.ctx)
self.assertEqual(result.data, {'roles': [UPDATED_ROLE_OBJ, ]})

View File

@ -112,10 +112,10 @@ class TestRolesUtils(base.TestCase):
role = yaml.safe_load(SAMPLE_ROLE)
del role[0]['name']
self.assertRaises(RoleMetadataError, rolesutils.validate_role_yaml,
yaml.dump(role))
yaml.safe_dump(role))
def test_validate_role_yaml_invalid_type(self):
role = yaml.safe_load(SAMPLE_ROLE)
role[0]['CountDefault'] = 'should not be a string'
self.assertRaises(RoleMetadataError, rolesutils.validate_role_yaml,
yaml.dump(role))
yaml.safe_dump(role))

View File

@ -1141,3 +1141,152 @@ workflows:
available_roles: <% $.get('available_roles', []) %>
on-success:
- fail: <% $.get('status') = "FAILED" %>
update_roles:
description: >
takes data in json format validates its contents and persists them in
roles_data.yaml, after successful update, templates are regenerated.
input:
- container
- roles
- roles_data_file: 'roles_data.yaml'
- replace_all: false
- queue_name: tripleo
tags:
- tripleo-common-managed
tasks:
get_available_roles:
workflow: list_available_roles
publish:
available_roles: <% task().result.available_roles %>
on-success: validate_input
on-error: notify_zaqar
publish-on-error:
status: FAILED
message: <% task().result %>
validate_input:
description: >
validate the format of input (verify that each role in input has the
required attributes set. check README in roles directory in t-h-t),
validate that roles in input exist in roles directory in t-h-t
action: tripleo.plan.validate_roles
input:
container: <% $.container %>
roles: <% $.roles %>
available_roles: <% $.available_roles %>
on-success: get_network_data
on-error: notify_zaqar
publish-on-error:
status: FAILED
message: <% task().result %>
get_network_data:
workflow: list_networks
input:
container: <% $.container %>
queue_name: <% $.queue_name %>
publish:
network_data: <% task().result.network_data %>
on-success: validate_network_names
publish-on-error:
status: FAILED
message: <% task().result %>
on-error: notify_zaqar
validate_network_names:
description: >
validate that Network names assigned to Role exist in
network-data.yaml object in Swift container
workflow: _validate_networks_from_roles
input:
container: <% $.container %>
defined_networks: <% $.network_data.name %>
networks_in_roles: <% $.roles.networks.flatten().distinct() %>
queue_name: <% $.queue_name %>
on-success: get_current_roles
on-error: notify_zaqar
publish-on-error:
status: FAILED
message: <% task().result.message %>
get_current_roles:
workflow: list_roles
input:
container: <% $.container %>
roles_data_file: <% $.roles_data_file %>
queue_name: <% $.queue_name %>
publish:
current_roles: <% task().result.roles_data %>
on-success: update_roles_data
on-error: notify_zaqar
publish-on-error:
status: FAILED
message: <% task().result %>
update_roles_data:
description: >
update roles_data.yaml object in Swift with roles from workflow input
action: tripleo.plan.update_roles
input:
container: <% $.container %>
roles: <% $.roles %>
current_roles: <% $.current_roles %>
replace_all: <% $.replace_all %>
publish:
updated_roles_data: <% task().result.roles %>
on-success: update_roles_data_in_swift
on-error: notify_zaqar
publish-on-error:
status: FAILED
message: <% task().result %>
update_roles_data_in_swift:
description: >
update roles_data.yaml object in Swift with data from workflow input
action: swift.put_object
input:
container: <% $.container %>
obj: <% $.roles_data_file %>
contents: <% yaml_dump($.updated_roles_data) %>
on-success: regenerate_templates
publish-on-error:
status: FAILED
message: <% task().result %>
on-error: notify_zaqar
regenerate_templates:
action: tripleo.templates.process container=<% $.container %>
on-success: get_updated_roles
on-error: notify_zaqar
publish-on-error:
status: FAILED
message: <% task().result %>
get_updated_roles:
workflow: list_roles
input:
container: <% $.container %>
roles_data_file: <% $.roles_data_file %>
publish:
updated_roles: <% task().result.roles_data %>
status: SUCCESS
on-complete: notify_zaqar
publish-on-error:
status: FAILED
message: <% task().result %>
notify_zaqar:
action: zaqar.queue_post
input:
queue_name: <% $.queue_name %>
messages:
body:
type: tripleo.roles.v1.update_roles
payload:
status: <% $.status %>
message: <% $.get('message', '') %>
execution: <% execution() %>
updated_roles: <% $.get('updated_roles', []) %>
on-success:
- fail: <% $.get('status') = "FAILED" %>