diff --git a/releasenotes/notes/update-roles-workflow-00be679eb8e9548c.yaml b/releasenotes/notes/update-roles-workflow-00be679eb8e9548c.yaml new file mode 100644 index 000000000..98de1cd26 --- /dev/null +++ b/releasenotes/notes/update-roles-workflow-00be679eb8e9548c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds a workflow and associated actions to update roles in a deployment + plan. diff --git a/setup.cfg b/setup.cfg index 41f576e69..3a5bafb92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tripleo_common/actions/plan.py b/tripleo_common/actions/plan.py index 458d490d4..afb55f675 100644 --- a/tripleo_common/actions/plan.py +++ b/tripleo_common/actions/plan.py @@ -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}) diff --git a/tripleo_common/tests/actions/test_plan.py b/tripleo_common/tests/actions/test_plan.py index 66868b52f..598945668 100644 --- a/tripleo_common/tests/actions/test_plan.py +++ b/tripleo_common/tests/actions/test_plan.py @@ -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, ]}) diff --git a/tripleo_common/tests/utils/test_roles.py b/tripleo_common/tests/utils/test_roles.py index d7fe82cb1..d01428dbb 100644 --- a/tripleo_common/tests/utils/test_roles.py +++ b/tripleo_common/tests/utils/test_roles.py @@ -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)) diff --git a/workbooks/plan_management.yaml b/workbooks/plan_management.yaml index 405801a20..85de26338 100644 --- a/workbooks/plan_management.yaml +++ b/workbooks/plan_management.yaml @@ -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" %> \ No newline at end of file