From 77878ed094cac6f135364de112b418c93d0c2ba1 Mon Sep 17 00:00:00 2001 From: Kristof Szabo Date: Tue, 28 Mar 2023 11:17:55 +0200 Subject: [PATCH 01/10] added backup modules best practice fixes further best practice fixes exception handling fix adding backup_plan_name to response when creating the plan formatting fixes initial seed no tags for backup selections initial seed WIP add integration tests Do not use paginators because they are not available in boto3 collection's requirements Signed-off-by: Alina Buzachis Fix backup_plan_* Signed-off-by: Alina Buzachis Re-work backup_selection_* Signed-off-by: Alina Buzachis --- meta/runtime.yml | 2 + plugins/module_utils/backup.py | 144 ++++++++ plugins/modules/backup_plan.py | 323 ++++++++++++++++++ plugins/modules/backup_plan_info.py | 139 ++++++++ plugins/modules/backup_selection.py | 217 ++++++++++++ plugins/modules/backup_selection_info.py | 167 +++++++++ tests/integration/targets/backup_plan/aliases | 3 + .../targets/backup_plan/defaults/main.yml | 4 + .../targets/backup_plan/tasks/main.yml | 54 +++ 9 files changed, 1053 insertions(+) create mode 100644 plugins/modules/backup_plan.py create mode 100644 plugins/modules/backup_plan_info.py create mode 100644 plugins/modules/backup_selection.py create mode 100644 plugins/modules/backup_selection_info.py create mode 100644 tests/integration/targets/backup_plan/aliases create mode 100644 tests/integration/targets/backup_plan/defaults/main.yml create mode 100644 tests/integration/targets/backup_plan/tasks/main.yml diff --git a/meta/runtime.yml b/meta/runtime.yml index f7895da01cd..11fc7ef1a9b 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -7,6 +7,8 @@ action_groups: - aws_az_info - aws_caller_info - aws_s3 + - backup_plan + - backup_plan_info - backup_tag - backup_tag_info - backup_vault diff --git a/plugins/module_utils/backup.py b/plugins/module_utils/backup.py index fb5c32a1984..872970d3592 100644 --- a/plugins/module_utils/backup.py +++ b/plugins/module_utils/backup.py @@ -9,6 +9,9 @@ except ImportError: pass # Handled by HAS_BOTO3 +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict + def get_backup_resource_tags(module, backup_client): resource = module.params.get("resource") @@ -18,3 +21,144 @@ def get_backup_resource_tags(module, backup_client): module.fail_json_aws(e, msg=f"Failed to list tags on the resource {resource}") return response["Tags"] + + +def _list_backup_plans(client, backup_plan_name): + first_iteration = False + next_token = None + + # We can not use the paginator at the moment because if was introduced after boto3 version 1.22 + # paginator = client.get_paginator("list_backup_plans") + # result = paginator.paginate(**params).build_full_result()["BackupPlansList"] + + response = client.list_backup_plans() + next_token = response.get("NextToken", None) + + if next_token is None: + entries = response["BackupPlansList"] + for backup_plan in entries: + if backup_plan_name == backup_plan["BackupPlanName"]: + return backup_plan["BackupPlanId"] + + while next_token is not None: + if first_iteration != False: + response = client.list_backup_plans(NextToken=next_token) + first_iteration = True + entries = response["BackupPlansList"] + for backup_plan in entries: + if backup_plan_name == backup_plan["BackupPlanName"]: + return backup_plan["BackupPlanId"] + try: + next_token = response.get('NextToken') + except: + next_token = None + + +def get_plan_details(module, client, backup_plan_name: str): + backup_plan_id = _list_backup_plans(client, backup_plan_name) + + if not backup_plan_id: + return [] + + try: + result = client.get_backup_plan(BackupPlanId=backup_plan_id) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg=f"Failed to describe plan {backup_plan_id}") + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_backup_plan = [] + + try: + module.params["resource"] = result.get("BackupPlanArn", None) + tag_dict = get_backup_resource_tags(module, client) + result.update({"tags": tag_dict}) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg=f"Failed to get the backup plan tags") + + snaked_backup_plan.append(camel_dict_to_snake_dict(result)) + + # Turn the boto3 result in to ansible friendly tag dictionary + for v in snaked_backup_plan: + if "tags_list" in v: + v["tags"] = boto3_tag_list_to_ansible_dict(v["tags_list"], "key", "value") + del v["tags_list"] + if "response_metadata" in v: + del v["response_metadata"] + v["backup_plan_name"] = v["backup_plan"]["backup_plan_name"] + + return snaked_backup_plan + + +def _list_backup_selections(client, backup_plan_id, backup_selection_name): + first_iteration = False + next_token = None + + # We can not use the paginator at the moment because if was introduced after boto3 version 1.22 + # paginator = client.get_paginator("list_backup_selections") + # result = paginator.paginate(**params).build_full_result()["BackupSelectionsList"] + + response = client.list_backup_selections(BackupPlanId=backup_plan_id) + next_token = response.get("NextToken", None) + + if next_token is None: + entries = response["BackupSelectionsList"] + for backup_selection in entries: + if backup_selection_name == backup_selection["SelectionName"]: + return backup_selection["SelectionId"] + + while next_token is not None: + if first_iteration != False: + response = client.list_backup_selections(BackupPlanId=backup_plan_id, NextToken=next_token) + first_iteration = True + entries = response["BackupSelectionsList"] + for backup_selection in entries: + if backup_selection_name == backup_selection["BackupPlanName"]: + return backup_selection["SelectionId"] + try: + next_token = response.get('NextToken') + except: + next_token = None + + +def get_selection_details(module, client, backup_plan_name, backup_selection_name: str): + backup_plan = get_plan_details(module, client, backup_plan_name) + + if not backup_plan: + module.fail_json(e, msg=f"The backup plan {backup_plan_name} does not exist. Please create one first.") + + backup_plan_id = backup_plan[0]["backup_plan_id"] + backup_selection_id = _list_backup_selections(client, backup_plan_id, backup_selection_name) + + if not backup_selection_id: + return [] + + try: + result = client.get_backup_selection(BackupPlanId=backup_plan_id, SelectionId=backup_selection_id) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg=f"Failed to describe plan {backup_selection_id}") + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_backup_selection = [] + + # try: + # module.params["resource"] = result.get("BackupPlanArn", None) + # tag_dict = get_backup_resource_tags(module, client) + # result.update({"tags": tag_dict}) + # except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + # module.fail_json_aws(e, msg=f"Failed to get the backup plan tags") + + snaked_backup_selection.append(camel_dict_to_snake_dict(result)) + + # Turn the boto3 result in to ansible friendly tag dictionary + for v in snaked_backup_selection: + if "tags_list" in v: + v["tags"] = boto3_tag_list_to_ansible_dict(v["tags_list"], "key", "value") + del v["tags_list"] + if "response_metadata" in v: + del v["response_metadata"] + if "backup_selection" in v: + for backup_selection_key in v['backup_selection']: + v[backup_selection_key] = v['backup_selection'][backup_selection_key] + del v["backup_selection"] + + return snaked_backup_selection diff --git a/plugins/modules/backup_plan.py b/plugins/modules/backup_plan.py new file mode 100644 index 00000000000..9ee4f8a07b3 --- /dev/null +++ b/plugins/modules/backup_plan.py @@ -0,0 +1,323 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r""" +--- +module: backup_plan +version_added: 6.0.0 +short_description: create, delete and modify AWS Backup plans +description: + - Manage AWS Backup plans. + - For more information see the AWS documentation for Backup plans U(https://docs.aws.amazon.com/aws-backup/latest/devguide/about-backup-plans.html). +author: + - Kristof Imre Szabo (@krisek) + - Alina Buzachis (@alinabuzachis) +options: + backup_plan_name: + description: + - The display name of a backup plan. Must contain 1 to 50 alphanumeric or '-_.' characters. + required: true + type: str + aliases: ['name'] + rules: + description: + - An array of BackupRule objects, each of which specifies a scheduled task that is used to back up a selection of resources. + required: false + type: list + advanced_backup_settings: + description: + - Specifies a list of BackupOptions for each resource type. These settings are only available for Windows Volume Shadow Copy Service (VSS) backup jobs. + required: false + type: list + state: + description: + - Create, delete a backup plan. + required: false + default: present + choices: ['present', 'absent'] + type: str +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 + - amazon.aws.tags +""" + +EXAMPLES = r""" +- name: create backup plan + amazon.aws.backup_plan: + state: present + backup_plan_name: elastic + rules: + - RuleName: every_morning + TargetBackupVaultName: elastic + ScheduleExpression: "cron(0 5 ? * * *)" + StartWindowMinutes: 120 + CompletionWindowMinutes: 10080 + Lifecycle: + DeleteAfterDays: 7 + EnableContinuousBackup: true + +""" +RETURN = r""" +backup_plan_arn: + description: ARN of the backup plan. + type: str + sample: arn:aws:backup:eu-central-1:111122223333:backup-plan:1111f877-1ecf-4d79-9718-a861cd09df3b +backup_plan_id: + description: Id of the backup plan. + type: str + sample: 1111f877-1ecf-4d79-9718-a861cd09df3b +backup_plan_name: + description: Name of the backup plan. + type: str + sample: elastic +creation_date: + description: Creation date of the backup plan. + type: str + sample: '2023-01-24T10:08:03.193000+01:00' +last_execution_date: + description: Last execution date of the backup plan. + type: str + sample: '2023-03-24T06:30:08.250000+01:00' +tags: + description: Tags of the backup plan + type: str +version_id: + description: Version id of the backup plan + type: str +backup_plan: + description: backup plan details + returned: always + type: complex + contains: + backup_plan_arn: + description: backup plan arn + returned: always + type: str + sample: arn:aws:backup:eu-central-1:111122223333:backup-plan:1111f877-1ecf-4d79-9718-a861cd09df3b + backup_plan_name: + description: backup plan name + returned: always + type: str + sample: elastic + advanced_backup_settings: + description: Advanced backup settings of the backup plan + type: list + elements: dict + contains: + resource_type: + description: Resource type of the advanced setting + type: str + backup_options: + description: Options of the advanced setting + type: dict + rules: + description: + - An array of BackupRule objects, each of which specifies a scheduled task that is used to back up a selection of resources. + type: list + +""" + + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # Handled by AnsibleAWSModule + +import json +from typing import Optional +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.backup import get_plan_details + + +def create_backup_plan(module: AnsibleAWSModule, client, params: dict): + """ + Creates a Backup Plan + + module : AnsibleAWSModule object + client : boto3 backup client connection object + params : The parameters to create a backup plan + """ + params = {k: v for k, v in params.items() if v is not None} + try: + response = client.create_backup_plan(**params) + except ( + BotoCoreError, + ClientError, + ) as err: + module.fail_json_aws(err, msg="Failed to create Backup Plan") + + return response + + +def plan_update_needed(client, backup_plan_id: str, backup_plan_data: dict) -> bool: + update_needed = False + + # we need to get current rules to manage the plan + full_plan = client.get_backup_plan(BackupPlanId=backup_plan_id) + + configured_rules = json.dumps( + [ + {key: val for key, val in rule.items() if key != "RuleId"} + for rule in full_plan.get("BackupPlan", {}).get("Rules", []) + ], + sort_keys=True, + ) + supplied_rules = json.dumps(backup_plan_data["BackupPlan"]["Rules"], sort_keys=True) + + if configured_rules != supplied_rules: + # rules to be updated + update_needed = True + + configured_advanced_backup_settings = json.dumps( + full_plan.get("BackupPlan", {}).get("AdvancedBackupSettings", None), + sort_keys=True, + ) + supplied_advanced_backup_settings = json.dumps( + backup_plan_data["BackupPlan"]["AdvancedBackupSettings"], sort_keys=True + ) + if configured_advanced_backup_settings != supplied_advanced_backup_settings: + # advanced settings to be updated + update_needed = True + return update_needed + + +def update_backup_plan( + module: AnsibleAWSModule, client, backup_plan_id: str, backup_plan_data: dict +): + try: + response = client.update_backup_plan( + BackupPlanId=backup_plan_id, + BackupPlan=backup_plan_data["BackupPlan"], + ) + except ( + BotoCoreError, + ClientError, + ) as err: + module.fail_json_aws(err, msg="Failed to create Backup Plan") + return response + + +def delete_backup_plan(module: AnsibleAWSModule, client, backup_plan_id: str): + """ + Delete a Backup Plan + + module : AnsibleAWSModule object + client : boto3 client connection object + backup_plan_id : Backup Plan ID + """ + try: + client.delete_backup_plan(BackupPlanId=backup_plan_id) + except (BotoCoreError, ClientError) as err: + module.fail_json_aws(err, msg="Failed to delete the Backup Plan") + + +def main(): + argument_spec = dict( + state=dict(default="present", choices=["present", "absent"]), + backup_plan_name=dict(required=True, type="str"), + rules=dict(type="list"), + advanced_backup_settings=dict(default=[], type="list"), + creator_request_id=dict(type="str"), + tags=dict(required=False, type="dict", aliases=["resource_tags"]), + purge_tags=dict(default=True, type="bool"), + ) + + required_if = [ + ("state", "present", ["backup_plan_name", "rules"]), + ("state", "absent", ["backup_plan_name"]), + ] + + module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True) + + # collect parameters + state = module.params.get("state") + backup_plan_name = module.params["backup_plan_name"] + purge_tags = module.params["purge_tags"] + try: + client = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) + except (ClientError, BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to connect to AWS") + + results = {"changed": False, "exists": False} + + current_plan = get_plan_details(module, client, backup_plan_name) + + if state == "present": + new_plan_data = { + "BackupPlan": { + "BackupPlanName": backup_plan_name, + "Rules": module.params["rules"], + "AdvancedBackupSettings": module.params.get("advanced_backup_settings"), + }, + "BackupPlanTags": module.params.get("tags"), + "CreatorRequestId": module.params.get("creator_request_id"), + } + + if not current_plan: # Plan does not exist, create it + results["exists"] = True + results["changed"] = True + + if module.check_mode: + module.exit_json(**results, msg="Would have created backup plan if not in check mode") + + create_backup_plan(module, client, new_plan_data) + + # TODO: add tags + # ensure_tags( + # client, + # module, + # response["BackupPlanArn"], + # purge_tags=module.params.get("purge_tags"), + # tags=module.params.get("tags"), + # resource_type="BackupPlan", + # ) + + else: # Plan exists, update if needed + results["exists"] = True + current_plan_id = current_plan[0]["backup_plan_id"] + if plan_update_needed(client, current_plan_id, new_plan_data): + results["changed"] = True + + if module.check_mode: + module.exit_json(**results, msg="Would have updated backup plan if not in check mode") + + update_backup_plan(module, client, current_plan_id, new_plan_data) + + if purge_tags: + pass + # TODO: Update plan tags + # ensure_tags( + # client, + # module, + # response["BackupPlanArn"], + # purge_tags=module.params.get("purge_tags"), + # tags=module.params.get("tags"), + # resource_type="BackupPlan", + # ) + + new_plan = get_plan_details(module, client, backup_plan_name) + results = results | new_plan[0] + + elif state == "absent": + if not current_plan: # Plan does not exist, can't delete it + module.exit_json(**results) + else: # Plan exists, delete it + results["changed"] = True + + if module.check_mode: + module.exit_json(**results, msg="Would have deleted backup plan if not in check mode") + + delete_backup_plan(module, client, current_plan[0]["backup_plan_id"]) + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/backup_plan_info.py b/plugins/modules/backup_plan_info.py new file mode 100644 index 00000000000..2fb5149ffa3 --- /dev/null +++ b/plugins/modules/backup_plan_info.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r""" +--- +module: backup_plan_info +version_added: 6.0.0 +short_description: Describe AWS Backup Plans +description: + - Lists info about Backup Plan configuration. +author: + - Gomathi Selvi Srinivasan (@GomathiselviS) + - Kristof Imre Szabo (@krisek) + - Alina Buzachis (@alinabuzachis) +options: + backup_plan_names: + type: list + elements: str + default: [] + description: + - Specifies a list of plan names. + - If an empty list is specified, information for the backup plans in the current region is returned. +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. +# Gather information about all backup plans +- amazon.aws.backup_plan_info +# Gather information about a particular backup plan +- amazon.aws.backup_plan_info: + backup plan_names: + - elastic +""" + +RETURN = r""" +backup_plans: + description: List of backup plan objects. Each element consists of a dict with all the information related to that backup plan. + type: list + elements: dict + returned: always + contains: + backup_plan_arn: + description: ARN of the backup plan. + type: str + sample: arn:aws:backup:eu-central-1:111122223333:backup-plan:1111f877-1ecf-4d79-9718-a861cd09df3b + backup_plan_id: + description: Id of the backup plan. + type: str + sample: 1111f877-1ecf-4d79-9718-a861cd09df3b + backup_plan_name: + description: Name of the backup plan. + type: str + sample: elastic + creation_date: + description: Creation date of the backup plan. + type: str + sample: '2023-01-24T10:08:03.193000+01:00' + last_execution_date: + description: Last execution date of the backup plan. + type: str + sample: '2023-03-24T06:30:08.250000+01:00' + tags: + description: Tags of the backup plan + type: str + version_id: + description: Version id of the backup plan + type: str + backup_plan: + elements: dict + returned: always + description: Detailed information about the backup plan. + contains: + backup_plan_name: + description: Name of the backup plan. + type: str + sample: elastic + advanced_backup_settings: + description: Advanced backup settings of the backup plan + type: list + elements: dict + contains: + resource_type: + description: Resource type of the advanced setting + type: str + backup_options: + description: Options of the advanced setting + type: dict + rules: + description: + - An array of BackupRule objects, each of which specifies a scheduled task that is used to back up a selection of resources. + type: list +""" + +try: + import botocore +except ImportError: + pass # Handled by AnsibleAWSModule + + +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.backup import get_plan_details + + +def get_backup_plan_detail(client, module): + backup_plan_list = [] + backup_plan_names = module.params.get("backup_plan_names") + + for name in backup_plan_names: + backup_plan_list.extend(get_plan_details(module, client, name)) + + module.exit_json(**{"backup_plans": backup_plan_list}) + + +def main(): + argument_spec = dict( + backup_plan_names=dict(type="list", elements="str", default=[]), + ) + + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) + + try: + connection = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to connect to AWS") + + get_backup_plan_detail(connection, module) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/backup_selection.py b/plugins/modules/backup_selection.py new file mode 100644 index 00000000000..8951a5280e9 --- /dev/null +++ b/plugins/modules/backup_selection.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import json +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule + +DOCUMENTATION = r""" +module: backup_selection +short_description: create, delete and modify AWS Backup selection +version_added: 6.0.0 +description: + - Manages AWS Backup selections. + - For more information see the AWS documentation for backup selections + U(https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html). +options: + backup_plan_id: + description: + - Uniquely identifies the backup plan to be associated with the selection of resources. + required: true + type: str + selection_name: + description: + - The display name of a resource selection document. Must contain 1 to 50 alphanumeric or '-_.' characters. + required: true + type: str + iam_role_arn: + description: + - The ARN of the IAM role that Backup uses to authenticate when backing up the target resource; + for example, arn:aws:iam::111122223333:role/system-backup . + required: true + type: str + resources: + description: + - A list of Amazon Resource Names (ARNs) to assign to a backup plan. The maximum number of ARNs is 500 without wildcards, + or 30 ARNs with wildcards. If you need to assign many resources to a backup plan, consider a different resource selection + strategy, such as assigning all resources of a resource type or refining your resource selection using tags. + required: false + type: list + list_of_tags: + description: + - A list of conditions that you define to assign resources to your backup plans using tags. + Condition operators are case sensitive. + required: false + type: list + not_resources: + description: + - A list of Amazon Resource Names (ARNs) to exclude from a backup plan. The maximum number of ARNs is 500 without wildcards, + or 30 ARNs with wildcards. If you need to exclude many resources from a backup plan, consider a different resource + selection strategy, such as assigning only one or a few resource types or refining your resource selection using tags. + required: false + type: list + conditions: + description: + - A list of conditions (expressed as a dict) that you define to assign resources to your backup plans using tags. + required: false + type: dict + state: + description: + - Create, delete a backup selection. + required: false + default: present + choices: ['present', 'absent'] + type: str +notes: [] +author: + - Kristof Imre Szabo (@krisek) + - Alina Buzachis (@alinabuzachis) +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + + +EXAMPLES = r""" +- name: create backup selection + backup_selection: + selection_name: elastic + backup_plan_id: 1111f877-1ecf-4d79-9718-a861cd09df3b + iam_role_arn: arn:aws:iam::111122223333:role/system-backup + resources: + - arn:aws:elasticfilesystem:*:*:file-system/* +""" + + +RETURN = r""" +selection_name: + description: backup selection name + returned: always + type: str + sample: elastic +backup_selection: + description: backup selection details + returned: always + type: complex + contains: + backup_plan_id: + description: backup plan id + returned: always + type: str + sample: 1111f877-1ecf-4d79-9718-a861cd09df3b + creation_date: + description: backup plan creation date + returned: always + type: str + sample: 2023-01-24T10:08:03.193000+01:00 + iam_role_arn: + description: iam role arn + returned: always + type: str + sample: arn:aws:iam::111122223333:role/system-backup + selection_id: + description: backup selection id + returned: always + type: str + sample: 1111c217-5d71-4a55-8728-5fc4e63d437b + selection_name: + description: backup selection name + returned: always + type: str + sample: elastic +""" +try: + import botocore +except ImportError: + pass # Handled by AnsibleAWSModule + +from ansible_collections.amazon.aws.plugins.module_utils.backup import get_selection_details + + +def main(): + argument_spec = dict( + selection_name=dict(type="str", required=True), + backup_plan_name=dict(type="str", required=True), + iam_role_arn=dict(type="str", required=True), + resources=dict(type="list", required=False), + purge_tags=dict(default=True, type="bool"), + state=dict(default="present", choices=["present", "absent"]), + ) + required_if = [ + ("state", "present", ["selection_name", "backup_plan_id", "iam_role_arn"]), + ("state", "absent", ["selection_name", "backup_plan_id"]), + ] + module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True) + state = module.params.get("state") + backup_selection_name = module.params.get("selection_name") + backup_plan_name = module.params.get("backup_plan_name") + iam_role_arn = module.params.get("iam_role_arn") + resources = module.params.get("resources") + list_of_tags = module.params.get("list_of_tags") + not_resources = module.params.get("not_resources") + conditions = module.params.get("conditions") + + try: + client = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to connect to AWS") + + results = {"changed": False, "exists": False} + + current_selection = get_selection_details(module, client, backup_plan_name, backup_selection_name) + + if state == "present": + # build data specified by user + backup_selection_data = {"SelectionName": backup_selection_name, "IamRoleArn": iam_role_arn} + if resources: + backup_selection_data["Resources"] = resources + if list_of_tags: + backup_selection_data["ListOfTags"] = list_of_tags + if not_resources: + backup_selection_data["NotResources"] = not_resources + if conditions: + backup_selection_data["Conditions"] = conditions + + if current_selection: + results["exists"] = True + results["backup_selection"] = current_selection[0] + else: + results["changed"] = True + results["exists"] = True + if module.check_mode: + module.exit_json(**results, msg="Would have created selection if not in check mode") + try: + client.create_backup_selection( + BackupSelection=backup_selection_data, BackupPlanId=current_selection[0]["backup_plan_id"] + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to create selection") + + new_selection = get_selection_details(module, client, backup_plan_name, backup_selection_name) + results["backup_selection"] = new_selection[0] + + elif state == "absent": + if current_selection: + results["changed"] = True + if module.check_mode: + module.exit_json(**results, msg="Would have deleted backup selection if not in check mode") + try: + client.delete_backup_selection( + aws_retry=True, SelectionId=current_selection[0]["selection_id"], BackupPlanId=current_selection[0]["backup_plan_id"] + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to delete selection") + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/backup_selection_info.py b/plugins/modules/backup_selection_info.py new file mode 100644 index 00000000000..1af9c3e1a9e --- /dev/null +++ b/plugins/modules/backup_selection_info.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r""" +--- +module: backup_selection_info +version_added: 6.0.0 +short_description: Describe AWS Backup Plans +description: + - Lists info about Backup Selection configuration for a given Backup Plan. +author: + - Gomathi Selvi Srinivasan (@GomathiselviS) + - Kristof Imre Szabo (@krisek) + - Alina Buzachis (@alinabuzachis) +options: + backup_plan_name: + description: + - Uniquely identifies the backup plan the selections should be listed for. + required: true + type: str +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. +# Gather information about all backup selections +- amazon.aws.backup_selection_info +# Gather information about a particular backup selection +- amazon.aws.backup_selection_info: + backup_selection_names: + - elastic +""" + +RETURN = r""" +backup_selections: + description: List of backup selection objects. Each element consists of a dict with all the information related to that backup selection. + type: list + elements: dict + returned: always + contains: + backup_plan_id: + description: backup plan id + returned: always + type: str + sample: 1111f877-1ecf-4d79-9718-a861cd09df3b + creation_date: + description: backup plan creation date + returned: always + type: str + sample: 2023-01-24T10:08:03.193000+01:00 + iam_role_arn: + description: iam role arn + returned: always + type: str + sample: arn:aws:iam::111122223333:role/system-backup + selection_id: + description: backup selection id + returned: always + type: str + sample: 1111c217-5d71-4a55-8728-5fc4e63d437b + selection_name: + description: backup selection name + returned: always + type: str + sample: elastic + conditions: + description: list of conditions (expressed as a dict) that are defined to assign resources to the backup plan using tags + returned: always + type: dict + sample: + list_of_tags: + description: conditions defined to assign resources to the backup plans using tags + returned: always + type: list + sample: + not_resources: + description: list of Amazon Resource Names (ARNs) that are excluded from the backup plan + returned: always + type: list + sample: + resources: + description: list of Amazon Resource Names (ARNs) that are assigned to the backup plan + returned: always + type: list + sample: +""" + +try: + import botocore +except ImportError: + pass # Handled by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict +from ansible_collections.amazon.aws.plugins.module_utils.backup import get_selection_details + + +def get_backup_selections(connection, module, backup_plan_id): + all_backup_selections = [] + try: + result = connection.get_paginator("list_backup_selections") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to get the backup plans.") + for page in result.paginate(BackupPlanId=backup_plan_id): + for backup_selection in page["BackupSelectionsList"]: + all_backup_selections.append(backup_selection["SelectionId"]) + return all_backup_selections + + +def get_backup_selection_detail(connection, module): + output = [] + result = {} + backup_plan_id = module.params.get("backup_plan_id") + backup_selection_list = get_backup_selections(connection, module, backup_plan_id) + + for backup_selection in backup_selection_list: + try: + output.append(connection.get_backup_selection(SelectionId=backup_selection, BackupPlanId=backup_plan_id, aws_retry=True)) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to describe selection SelectionId={0} BackupPlanId={1}".format(backup_selection, backup_plan_id)) + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_backup_selection = [] + + for backup_selection in output: + snaked_backup_selection.append(camel_dict_to_snake_dict(backup_selection)) + + # Turn the boto3 result in to ansible friendly dictionary + for v in snaked_backup_selection: + if "tags_list" in v: + v["tags"] = boto3_tag_list_to_ansible_dict(v["tags_list"], "key", "value") + del v["tags_list"] + if "response_metadata" in v: + del v["response_metadata"] + if "backup_selection" in v: + for backup_selection_key in v['backup_selection']: + v[backup_selection_key] = v['backup_selection'][backup_selection_key] + del v["backup_selection"] + result["backup_selections"] = snaked_backup_selection + return result + + +def main(): + argument_spec = dict( + backup_plan_id=dict(type="str", required=True), + ) + + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) + + try: + connection = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to connect to AWS") + + get_backup_selection_detail(connection, module) + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/backup_plan/aliases b/tests/integration/targets/backup_plan/aliases new file mode 100644 index 00000000000..d9b03076320 --- /dev/null +++ b/tests/integration/targets/backup_plan/aliases @@ -0,0 +1,3 @@ +cloud/aws +backup_plan +backup_vault diff --git a/tests/integration/targets/backup_plan/defaults/main.yml b/tests/integration/targets/backup_plan/defaults/main.yml new file mode 100644 index 00000000000..35af3bc6551 --- /dev/null +++ b/tests/integration/targets/backup_plan/defaults/main.yml @@ -0,0 +1,4 @@ +--- +# defaults file for test_backup_plan +backup_vault_name: '{{ tiny_prefix }}-backup-vault' +backup_plan_name: '{{ tiny_prefix }}-backup-plan' diff --git a/tests/integration/targets/backup_plan/tasks/main.yml b/tests/integration/targets/backup_plan/tasks/main.yml new file mode 100644 index 00000000000..45cfa380611 --- /dev/null +++ b/tests/integration/targets/backup_plan/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- module_defaults: + group/aws: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + block: + - name: Create an AWS Backup vault for the plan to target + amazon.aws.backup_vault: + backup_vault_name: "{{ backup_vault_name }}" + + - name: Create an AWS Backup plan + amazon.aws.backup_plan: + backup_plan_name: "{{ backup_plan_name }}" + rules: + - RuleName: daily + TargetBackupVaultName: "{{ backup_vault_name }}" + ScheduleExpression: "cron(0 5 ? * * *)" + StartWindowMinutes: 60 + CompletionWindowMinutes: 1440 + tags: + environment: test + register: backup_plan_create_result + + - name: Print result + ansible.builtin.debug: + msg: "{{ backup_plan_create_result }}" + + - name: Verify results + ansible.builtin.assert: + that: + - backup_plan_create_result.exists + - backup_plan_create_result.changed + - backup_plan_create_result.backup_plan.backup_plan_name == backup_plan_name + + - name: Get info on AWS Backup plan + amazon.aws.backup_plan_info: + backup_plan_names: + - "{{ backup_plan_name }}" + register: backup_plan_info_result + + always: + - name: Delete AWS Backup plan created during this test + amazon.aws.backup_plan: + backup_plan_name: "{{ backup_plan_name }}" + state: absent + ignore_errors: true + + - name: Delete AWS Backup vault created during this test + amazon.aws.backup_vault: + backup_vault_name: "{{ backup_vault_name }}" + state: absent + ignore_errors: true From fff47f8974bf0e055babdedb98c28c41ca0ff4c8 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Sun, 7 May 2023 22:16:32 +0200 Subject: [PATCH 02/10] Add backup_selection* modules Signed-off-by: Alina Buzachis --- meta/runtime.yml | 2 + plugins/module_utils/backup.py | 142 ++++----- plugins/modules/backup_selection.py | 198 +++++++++---- plugins/modules/backup_selection_info.py | 116 +++----- .../targets/backup_selection/aliases | 6 + .../backup_selection/defaults/main.yml | 6 + .../backup_selection/files/backup-policy.json | 12 + .../targets/backup_selection/tasks/main.yml | 271 ++++++++++++++++++ 8 files changed, 564 insertions(+), 189 deletions(-) create mode 100644 tests/integration/targets/backup_selection/aliases create mode 100644 tests/integration/targets/backup_selection/defaults/main.yml create mode 100644 tests/integration/targets/backup_selection/files/backup-policy.json create mode 100644 tests/integration/targets/backup_selection/tasks/main.yml diff --git a/meta/runtime.yml b/meta/runtime.yml index 11fc7ef1a9b..0242673f7ec 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -11,6 +11,8 @@ action_groups: - backup_plan_info - backup_tag - backup_tag_info + - backup_selection + - backup_selection_info - backup_vault - backup_vault_info - cloudformation diff --git a/plugins/module_utils/backup.py b/plugins/module_utils/backup.py index 872970d3592..4d04ea2e3c8 100644 --- a/plugins/module_utils/backup.py +++ b/plugins/module_utils/backup.py @@ -9,9 +9,16 @@ except ImportError: pass # Handled by HAS_BOTO3 +from typing import Union from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict +import logging + +logging.basicConfig( + filename="/tmp/file.log", level=logging.DEBUG, format="%(asctime)s:%(levelname)s:%(name)s:%(message)s" +) + def get_backup_resource_tags(module, backup_client): resource = module.params.get("resource") @@ -41,17 +48,14 @@ def _list_backup_plans(client, backup_plan_name): return backup_plan["BackupPlanId"] while next_token is not None: - if first_iteration != False: + if first_iteration: response = client.list_backup_plans(NextToken=next_token) first_iteration = True entries = response["BackupPlansList"] for backup_plan in entries: if backup_plan_name == backup_plan["BackupPlanName"]: return backup_plan["BackupPlanId"] - try: - next_token = response.get('NextToken') - except: - next_token = None + next_token = response.get("NextToken") def get_plan_details(module, client, backup_plan_name: str): @@ -69,11 +73,11 @@ def get_plan_details(module, client, backup_plan_name: str): snaked_backup_plan = [] try: - module.params["resource"] = result.get("BackupPlanArn", None) - tag_dict = get_backup_resource_tags(module, client) - result.update({"tags": tag_dict}) + module.params["resource"] = result.get("BackupPlanArn", None) + tag_dict = get_backup_resource_tags(module, client) + result.update({"tags": tag_dict}) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg=f"Failed to get the backup plan tags") + module.fail_json_aws(e, msg="Failed to get the backup plan tags") snaked_backup_plan.append(camel_dict_to_snake_dict(result)) @@ -89,76 +93,80 @@ def get_plan_details(module, client, backup_plan_name: str): return snaked_backup_plan -def _list_backup_selections(client, backup_plan_id, backup_selection_name): +def _list_backup_selections(client, module, plan_id): first_iteration = False next_token = None + selections = [] # We can not use the paginator at the moment because if was introduced after boto3 version 1.22 # paginator = client.get_paginator("list_backup_selections") # result = paginator.paginate(**params).build_full_result()["BackupSelectionsList"] - response = client.list_backup_selections(BackupPlanId=backup_plan_id) + try: + response = client.list_backup_selections(BackupPlanId=plan_id) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to list AWS backup selections") + next_token = response.get("NextToken", None) if next_token is None: - entries = response["BackupSelectionsList"] - for backup_selection in entries: - if backup_selection_name == backup_selection["SelectionName"]: - return backup_selection["SelectionId"] - - while next_token is not None: - if first_iteration != False: - response = client.list_backup_selections(BackupPlanId=backup_plan_id, NextToken=next_token) + return response["BackupSelectionsList"] + + while next_token: + if first_iteration: + try: + response = client.list_backup_selections(BackupPlanId=plan_id, NextToken=next_token) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to list AWS backup selections") first_iteration = True - entries = response["BackupSelectionsList"] - for backup_selection in entries: - if backup_selection_name == backup_selection["BackupPlanName"]: - return backup_selection["SelectionId"] - try: - next_token = response.get('NextToken') - except: - next_token = None + selections.append(response["BackupSelectionsList"]) + next_token = response.get("NextToken") -def get_selection_details(module, client, backup_plan_name, backup_selection_name: str): - backup_plan = get_plan_details(module, client, backup_plan_name) - - if not backup_plan: - module.fail_json(e, msg=f"The backup plan {backup_plan_name} does not exist. Please create one first.") - - backup_plan_id = backup_plan[0]["backup_plan_id"] - backup_selection_id = _list_backup_selections(client, backup_plan_id, backup_selection_name) - - if not backup_selection_id: - return [] - +def _get_backup_selection(client, module, plan_id, selection_id): try: - result = client.get_backup_selection(BackupPlanId=backup_plan_id, SelectionId=backup_selection_id) + result = client.get_backup_selection(BackupPlanId=plan_id, SelectionId=selection_id) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg=f"Failed to describe plan {backup_selection_id}") - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_backup_selection = [] - - # try: - # module.params["resource"] = result.get("BackupPlanArn", None) - # tag_dict = get_backup_resource_tags(module, client) - # result.update({"tags": tag_dict}) - # except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - # module.fail_json_aws(e, msg=f"Failed to get the backup plan tags") - - snaked_backup_selection.append(camel_dict_to_snake_dict(result)) - - # Turn the boto3 result in to ansible friendly tag dictionary - for v in snaked_backup_selection: - if "tags_list" in v: - v["tags"] = boto3_tag_list_to_ansible_dict(v["tags_list"], "key", "value") - del v["tags_list"] - if "response_metadata" in v: - del v["response_metadata"] - if "backup_selection" in v: - for backup_selection_key in v['backup_selection']: - v[backup_selection_key] = v['backup_selection'][backup_selection_key] - del v["backup_selection"] - - return snaked_backup_selection + module.fail_json_aws(e, msg=f"Failed to describe selection {selection_id}") + return result or [] + + +def get_selection_details(module, client, plan_name: str, selection_name: Union[str, list]): + result = [] + + plan = get_plan_details(module, client, plan_name) + + if not plan: + module.fail_json(msg=f"The backup plan {plan_name} does not exist. Please create one first.") + + plan_id = plan[0]["backup_plan_id"] + + selection_list = _list_backup_selections(client, module, plan_id) + + if selection_name: + for selection in selection_list: + if isinstance(selection_name, list): + for name in selection_name: + if selection["SelectionName"] == name: + selection_id = selection["SelectionId"] + selection_info = _get_backup_selection(client, module, plan_id, selection_id) + result.append(selection_info) + if isinstance(selection_name, str): + if selection["SelectionName"] == selection_name: + selection_id = selection["SelectionId"] + result.append(_get_backup_selection(client, module, plan_id, selection_id)) + break + else: + for selection in selection_list: + selection_id = selection["SelectionId"] + result.append(_get_backup_selection(client, module, plan_id, selection_id)) + + for v in result: + if "ResponseMetadata" in v: + del v["ResponseMetadata"] + if "BackupSelection" in v: + for backup_selection_key in v["BackupSelection"]: + v[backup_selection_key] = v["BackupSelection"][backup_selection_key] + del v["BackupSelection"] + + return result diff --git a/plugins/modules/backup_selection.py b/plugins/modules/backup_selection.py index 8951a5280e9..b673e758a35 100644 --- a/plugins/modules/backup_selection.py +++ b/plugins/modules/backup_selection.py @@ -4,73 +4,66 @@ # Copyright: Contributors to the Ansible project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -import json -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry -from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule DOCUMENTATION = r""" module: backup_selection -short_description: create, delete and modify AWS Backup selection +short_description: Create, delete and modify AWS Backup selection version_added: 6.0.0 description: - Manages AWS Backup selections. - For more information see the AWS documentation for backup selections U(https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html). options: - backup_plan_id: + backup_plan_name: description: - Uniquely identifies the backup plan to be associated with the selection of resources. required: true type: str + aliases: + - plan_name selection_name: description: - The display name of a resource selection document. Must contain 1 to 50 alphanumeric or '-_.' characters. required: true type: str + aliases: + - selection_name iam_role_arn: description: - - The ARN of the IAM role that Backup uses to authenticate when backing up the target resource; - for example, arn:aws:iam::111122223333:role/system-backup . - required: true + - The ARN of the IAM role that Backup uses to authenticate when backing up the target resource. type: str resources: description: - A list of Amazon Resource Names (ARNs) to assign to a backup plan. The maximum number of ARNs is 500 without wildcards, or 30 ARNs with wildcards. If you need to assign many resources to a backup plan, consider a different resource selection strategy, such as assigning all resources of a resource type or refining your resource selection using tags. - required: false type: list + elements: str list_of_tags: description: - A list of conditions that you define to assign resources to your backup plans using tags. - Condition operators are case sensitive. - required: false + - Condition operators are case sensitive. type: list + elements: dict not_resources: description: - A list of Amazon Resource Names (ARNs) to exclude from a backup plan. The maximum number of ARNs is 500 without wildcards, or 30 ARNs with wildcards. If you need to exclude many resources from a backup plan, consider a different resource selection strategy, such as assigning only one or a few resource types or refining your resource selection using tags. - required: false type: list + elements: str conditions: description: - A list of conditions (expressed as a dict) that you define to assign resources to your backup plans using tags. - required: false type: dict state: description: - Create, delete a backup selection. - required: false default: present choices: ['present', 'absent'] type: str -notes: [] -author: + +authors: - Kristof Imre Szabo (@krisek) - Alina Buzachis (@alinabuzachis) extends_documentation_fragment: @@ -81,10 +74,10 @@ EXAMPLES = r""" -- name: create backup selection - backup_selection: +- name: Create backup selection + amazon.aws.backup_selection: selection_name: elastic - backup_plan_id: 1111f877-1ecf-4d79-9718-a861cd09df3b + backup_plan_name: 1111f877-1ecf-4d79-9718-a861cd09df3b iam_role_arn: arn:aws:iam::111122223333:role/system-backup resources: - arn:aws:elasticfilesystem:*:*:file-system/* @@ -92,62 +85,141 @@ RETURN = r""" -selection_name: - description: backup selection name - returned: always - type: str - sample: elastic backup_selection: - description: backup selection details + description: Backup selection details. returned: always type: complex contains: backup_plan_id: - description: backup plan id + description: Backup plan id. returned: always type: str sample: 1111f877-1ecf-4d79-9718-a861cd09df3b creation_date: - description: backup plan creation date + description: Backup plan creation date. returned: always type: str sample: 2023-01-24T10:08:03.193000+01:00 iam_role_arn: - description: iam role arn + description: The ARN of the IAM role that Backup uses. returned: always type: str sample: arn:aws:iam::111122223333:role/system-backup selection_id: - description: backup selection id + description: Backup selection id. returned: always type: str sample: 1111c217-5d71-4a55-8728-5fc4e63d437b selection_name: - description: backup selection name + description: Backup selection name. returned: always type: str sample: elastic + conditions: + description: List of conditions (expressed as a dict) that are defined to assign resources to the backup plan using tags. + returned: always + type: dict + sample: {} + list_of_tags: + description: Conditions defined to assign resources to the backup plans using tags. + returned: always + type: list + elements: dict + sample: [] + not_resources: + description: List of Amazon Resource Names (ARNs) that are excluded from the backup plan. + returned: always + type: list + sample: [] + resources: + description: List of Amazon Resource Names (ARNs) that are assigned to the backup plan. + returned: always + type: list + sample: [] """ + +import json + try: import botocore except ImportError: pass # Handled by AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.backup import get_selection_details +from ansible_collections.amazon.aws.plugins.module_utils.backup import get_plan_details +from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +import logging + +logging.basicConfig( + filename="/tmp/file.log", level=logging.DEBUG, format="%(asctime)s:%(levelname)s:%(name)s:%(message)s" +) + + +def check_for_update(current_selection, backup_selection_data, iam_role_arn): + update_needed = False + if current_selection[0].get("IamRoleArn", None) != iam_role_arn: + update_needed = True + + fields_to_check = [ + { + "field_name": "Resources", + "field_value_from_aws": json.dumps(current_selection[0].get("Resources", None), sort_keys=True), + "field_value": json.dumps(backup_selection_data.get("Resources", []), sort_keys=True), + }, + { + "field_name": "ListOfTags", + "field_value_from_aws": json.dumps(current_selection[0].get("ListOfTags", None), sort_keys=True), + "field_value": json.dumps(backup_selection_data.get("ListOfTags", []), sort_keys=True), + }, + { + "field_name": "NotResources", + "field_value_from_aws": json.dumps(current_selection[0].get("NotResources", None), sort_keys=True), + "field_value": json.dumps(backup_selection_data.get("NotResources", []), sort_keys=True), + }, + { + "field_name": "Conditions", + "field_value_from_aws": json.dumps(current_selection[0].get("Conditions", None), sort_keys=True), + "field_value": json.dumps(backup_selection_data.get("Conditions", []), sort_keys=True), + }, + ] + for field_to_check in fields_to_check: + if field_to_check["field_value_from_aws"] != field_to_check["field_value"]: + if ( + field_to_check["field_name"] != "Conditions" + and field_to_check["field_value_from_aws"] != "[]" + and field_to_check["field_value"] != "null" + ): + # advanced settings to be updated + update_needed = True + if ( + field_to_check["field_name"] == "Conditions" + and field_to_check["field_value_from_aws"] + != '{"StringEquals": [], "StringLike": [], "StringNotEquals": [], "StringNotLike": []}' + and field_to_check["field_value"] != "null" + ): + update_needed = True + + return update_needed def main(): argument_spec = dict( - selection_name=dict(type="str", required=True), - backup_plan_name=dict(type="str", required=True), - iam_role_arn=dict(type="str", required=True), - resources=dict(type="list", required=False), - purge_tags=dict(default=True, type="bool"), + backup_selection_name=dict(type="str", required=True, aliases=["selection_name"]), + backup_plan_name=dict(type="str", required=True, aliases=["plan_name"]), + iam_role_arn=dict(type="str"), + resources=dict(type="list", elements="str"), + conditions=dict(type="dict"), + not_resources=dict(type="list", elements="str"), + list_of_tags=dict(type="list", elements="dict"), state=dict(default="present", choices=["present", "absent"]), ) required_if = [ - ("state", "present", ["selection_name", "backup_plan_id", "iam_role_arn"]), - ("state", "absent", ["selection_name", "backup_plan_id"]), + ("state", "present", ["backup_selection_name", "backup_plan_name", "iam_role_arn"]), + ("state", "absent", ["backup_selection_name", "backup_plan_name"]), ] module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True) state = module.params.get("state") @@ -164,39 +236,59 @@ def main(): except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to connect to AWS") - results = {"changed": False, "exists": False} + results = {"changed": False, "exists": False, "backup_selection": {}} current_selection = get_selection_details(module, client, backup_plan_name, backup_selection_name) if state == "present": # build data specified by user + update_needed = False backup_selection_data = {"SelectionName": backup_selection_name, "IamRoleArn": iam_role_arn} if resources: backup_selection_data["Resources"] = resources if list_of_tags: - backup_selection_data["ListOfTags"] = list_of_tags + backup_selection_data["ListOfTags"] = snake_dict_to_camel_dict(list_of_tags, capitalize_first=True) if not_resources: backup_selection_data["NotResources"] = not_resources if conditions: - backup_selection_data["Conditions"] = conditions + backup_selection_data["Conditions"] = snake_dict_to_camel_dict(conditions, capitalize_first=True) if current_selection: results["exists"] = True - results["backup_selection"] = current_selection[0] - else: + update_needed |= check_for_update(current_selection, backup_selection_data, iam_role_arn) + + if update_needed: + if module.check_mode: + results["changed"] = True + module.exit_json(**results, msg="Would have created selection if not in check mode") + + try: + client.delete_backup_selection( + aws_retry=True, + SelectionId=current_selection[0]["SelectionId"], + BackupPlanId=current_selection[0]["BackupPlanId"], + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to delete selection") + elif not update_needed: + results["exists"] = True + # state is present but backup vault doesnt exist + if not current_selection or update_needed: results["changed"] = True results["exists"] = True + plan = get_plan_details(module, client, backup_plan_name) + if module.check_mode: module.exit_json(**results, msg="Would have created selection if not in check mode") try: - client.create_backup_selection( - BackupSelection=backup_selection_data, BackupPlanId=current_selection[0]["backup_plan_id"] - ) + client.create_backup_selection( + BackupSelection=backup_selection_data, BackupPlanId=plan[0]["backup_plan_id"] + ) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to create selection") - new_selection = get_selection_details(module, client, backup_plan_name, backup_selection_name) - results["backup_selection"] = new_selection[0] + new_selection = get_selection_details(module, client, backup_plan_name, backup_selection_name) + results["backup_selection"] = camel_dict_to_snake_dict(*new_selection) elif state == "absent": if current_selection: @@ -205,7 +297,9 @@ def main(): module.exit_json(**results, msg="Would have deleted backup selection if not in check mode") try: client.delete_backup_selection( - aws_retry=True, SelectionId=current_selection[0]["selection_id"], BackupPlanId=current_selection[0]["backup_plan_id"] + aws_retry=True, + SelectionId=current_selection[0]["SelectionId"], + BackupPlanId=current_selection[0]["BackupPlanId"], ) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to delete selection") diff --git a/plugins/modules/backup_selection_info.py b/plugins/modules/backup_selection_info.py index 1af9c3e1a9e..0654810b1bc 100644 --- a/plugins/modules/backup_selection_info.py +++ b/plugins/modules/backup_selection_info.py @@ -19,9 +19,18 @@ options: backup_plan_name: description: - - Uniquely identifies the backup plan the selections should be listed for. + - Uniquely identifies the backup plan to be associated with the selection of resources. required: true type: str + aliases: + - plan_name + backup_selection_names: + description: + - Uniquely identifies the backup plan the selections should be listed for. + type: list + elemenst: str + aliases: + - selection_names extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules @@ -30,12 +39,15 @@ EXAMPLES = r""" # Note: These examples do not set authentication details, see the AWS Guide for details. -# Gather information about all backup selections -- amazon.aws.backup_selection_info -# Gather information about a particular backup selection -- amazon.aws.backup_selection_info: +- name: Gather information about all backup selections + amazon.aws.backup_selection_info: + backup_plan_name: "{{ backup_plan_name }}" + +- name: Gather information about a particular backup selection + amazon.aws.backup_selection_info: + backup_plan_name: "{{ backup_plan_name }}" backup_selection_names: - - elastic + - "{{ backup_selection_name }}" """ RETURN = r""" @@ -46,122 +58,86 @@ returned: always contains: backup_plan_id: - description: backup plan id + description: Backup plan id. returned: always type: str sample: 1111f877-1ecf-4d79-9718-a861cd09df3b creation_date: - description: backup plan creation date + description: Backup plan creation date. returned: always type: str sample: 2023-01-24T10:08:03.193000+01:00 iam_role_arn: - description: iam role arn + description: IAM role arn. returned: always type: str sample: arn:aws:iam::111122223333:role/system-backup selection_id: - description: backup selection id + description: Backup selection id. returned: always type: str sample: 1111c217-5d71-4a55-8728-5fc4e63d437b selection_name: - description: backup selection name + description: Backup selection name. returned: always type: str sample: elastic conditions: - description: list of conditions (expressed as a dict) that are defined to assign resources to the backup plan using tags + description: List of conditions (expressed as a dict) that are defined to assign resources to the backup plan using tags. returned: always type: dict - sample: + sample: {} list_of_tags: - description: conditions defined to assign resources to the backup plans using tags + description: Conditions defined to assign resources to the backup plans using tags. returned: always type: list - sample: + elements: dict + sample: [] not_resources: - description: list of Amazon Resource Names (ARNs) that are excluded from the backup plan + description: List of Amazon Resource Names (ARNs) that are excluded from the backup plan. returned: always type: list - sample: + sample: [] resources: - description: list of Amazon Resource Names (ARNs) that are assigned to the backup plan + description: List of Amazon Resource Names (ARNs) that are assigned to the backup plan. returned: always type: list - sample: + sample: [] """ + try: import botocore except ImportError: pass # Handled by AnsibleAWSModule -from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry -from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict from ansible_collections.amazon.aws.plugins.module_utils.backup import get_selection_details -def get_backup_selections(connection, module, backup_plan_id): - all_backup_selections = [] - try: - result = connection.get_paginator("list_backup_selections") - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get the backup plans.") - for page in result.paginate(BackupPlanId=backup_plan_id): - for backup_selection in page["BackupSelectionsList"]: - all_backup_selections.append(backup_selection["SelectionId"]) - return all_backup_selections - - -def get_backup_selection_detail(connection, module): - output = [] - result = {} - backup_plan_id = module.params.get("backup_plan_id") - backup_selection_list = get_backup_selections(connection, module, backup_plan_id) - - for backup_selection in backup_selection_list: - try: - output.append(connection.get_backup_selection(SelectionId=backup_selection, BackupPlanId=backup_plan_id, aws_retry=True)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe selection SelectionId={0} BackupPlanId={1}".format(backup_selection, backup_plan_id)) - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_backup_selection = [] - - for backup_selection in output: - snaked_backup_selection.append(camel_dict_to_snake_dict(backup_selection)) - - # Turn the boto3 result in to ansible friendly dictionary - for v in snaked_backup_selection: - if "tags_list" in v: - v["tags"] = boto3_tag_list_to_ansible_dict(v["tags_list"], "key", "value") - del v["tags_list"] - if "response_metadata" in v: - del v["response_metadata"] - if "backup_selection" in v: - for backup_selection_key in v['backup_selection']: - v[backup_selection_key] = v['backup_selection'][backup_selection_key] - del v["backup_selection"] - result["backup_selections"] = snaked_backup_selection - return result - - def main(): argument_spec = dict( - backup_plan_id=dict(type="str", required=True), + backup_plan_name=dict(type="str", required=True, aliases=["plan_name"]), + backup_selection_names=dict(type="list", elements="str", aliases=["selection_names"]), ) + global client + global module + result = {} + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) try: - connection = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) + client = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Failed to connect to AWS") - - get_backup_selection_detail(connection, module) + + result["backup_selections"] = get_selection_details( + module, client, module.params.get("backup_plan_name"), module.params.get("backup_selection_names") + ) + module.exit_json(**result) + if __name__ == "__main__": main() diff --git a/tests/integration/targets/backup_selection/aliases b/tests/integration/targets/backup_selection/aliases new file mode 100644 index 00000000000..e966bd6141c --- /dev/null +++ b/tests/integration/targets/backup_selection/aliases @@ -0,0 +1,6 @@ +cloud/aws + +backup_selection +backup_selection_info +backup_vault +backup_plan diff --git a/tests/integration/targets/backup_selection/defaults/main.yml b/tests/integration/targets/backup_selection/defaults/main.yml new file mode 100644 index 00000000000..26c6809cd85 --- /dev/null +++ b/tests/integration/targets/backup_selection/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# defaults file for backup_selection integration tests +backup_iam_role_name: 'ansible-test-{{ tiny_prefix }}-backup-iam-role' +backup_vault_name: '{{ tiny_prefix }}-backup-vault' +backup_plan_name: '{{ tiny_prefix }}-backup-plan' +backup_selection_name: '{{ tiny_prefix }}-backup-selection' diff --git a/tests/integration/targets/backup_selection/files/backup-policy.json b/tests/integration/targets/backup_selection/files/backup-policy.json new file mode 100644 index 00000000000..c8c348127a7 --- /dev/null +++ b/tests/integration/targets/backup_selection/files/backup-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement":[ + { + "Effect": "Allow", + "Principal": { + "Service": "backup.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} diff --git a/tests/integration/targets/backup_selection/tasks/main.yml b/tests/integration/targets/backup_selection/tasks/main.yml new file mode 100644 index 00000000000..db52d563f54 --- /dev/null +++ b/tests/integration/targets/backup_selection/tasks/main.yml @@ -0,0 +1,271 @@ +--- +- module_defaults: + group/aws: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + + block: + - name: Create an IAM Role + community.aws.iam_role: + name: "{{ backup_iam_role_name }}" + assume_role_policy_document: '{{ lookup("file", "backup-policy.json") }}' + create_instance_profile: no + description: "Ansible AWS Backup Role" + managed_policy: + - "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup" + register: iam_role + + - name: Create an AWS Backup vault for the plan to target + amazon.aws.backup_vault: + backup_vault_name: "{{ backup_vault_name }}" + register: _resutl_create_backup_vault + + - name: Verify result + ansible.builtin.assert: + that: + - _resutl_create_backup_vault.changed + + # - name: Create an AWS Backup plan + # amazon.aws.backup_plan: + # backup_plan_name: "{{ backup_plan_name }}" + # rules: + # - RuleName: DailyBackups + # TargetBackupVaultName: "{{ backup_vault_name }}" + # ScheduleExpression: "cron(0 5 ? * * *)" + # StartWindowMinutes: 60 + # CompletionWindowMinutes: 1440 + # tags: + # environment: test + # register: _resutl_create_backup_plan + + # - name: Verify result + # ansible.builtin.assert: + # that: + # - _resutl_create_backup_plan.changed + + # - name: Get detailed information about the AWS Backup plan + # amazon.aws.backup_plan_info: + # backup_plan_names: + # - "{{ backup_plan_name }}" + # register: _result_backup_plan_info + + # - name: Verify result + # ansible.builtin.assert: + # that: + # - _result_backup_plan_info.backup_plans | length == 1 + + - name: Create an AWS Backup plan + command: aws backup create-backup-plan --backup-plan "{\"BackupPlanName\":\"{{ backup_plan_name }}\",\"Rules\":[{\"RuleName\":\"DailyBackups\",\"ScheduleExpression\":\"cron(0 5 ? * * *)\",\"StartWindowMinutes\":60,\"TargetBackupVaultName\":\"{{ backup_vault_name }}\",\"CompletionWindowMinutes\":1440,\"Lifecycle\":{\"DeleteAfterDays\":35}}]}" + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + register: _result_create_backup_plan + + - set_fact: + backup_plan_id: "{{ (_result_create_backup_plan.stdout | from_json).BackupPlanId }}" + + - name: Create an AWS Backup selection (check_mode) + amazon.aws.backup_selection: + selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + iam_role_arn: "{{ iam_role.iam_role.arn }}" + list_of_tags: + - condition_type: "STRINGEQUALS" + condition_key: "backup" + condition_value: "daily" + check_mode: true + register: _create_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - _create_result_backup_selection.changed + + - name: Create an AWS Backup selection + amazon.aws.backup_selection: + selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + iam_role_arn: "{{ iam_role.iam_role.arn }}" + list_of_tags: + - condition_type: "STRINGEQUALS" + condition_key: "backup" + condition_value: "daily" + register: _create_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - _create_result_backup_selection.changed + - "'backup_selection' in _create_result_backup_selection" + - _create_result_backup_selection.backup_selection.iam_role_arn == iam_role.iam_role.arn + - _create_result_backup_selection.backup_selection.selection_name == "{{ backup_selection_name }}" + + - name: Create an AWS Backup selection (idempotency) + amazon.aws.backup_selection: + selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + iam_role_arn: "{{ iam_role.iam_role.arn }}" + list_of_tags: + - condition_type: "STRINGEQUALS" + condition_key: "backup" + condition_value: "daily" + register: _create_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - not _create_result_backup_selection.changed + - "'backup_selection' in _create_result_backup_selection" + - _create_result_backup_selection.backup_selection.iam_role_arn == iam_role.iam_role.arn + - _create_result_backup_selection.backup_selection.selection_name == "{{ backup_selection_name }}" + + - name: Get detailed information about the AWS Backup selection + amazon.aws.backup_selection_info: + backup_plan_name: "{{ backup_plan_name }}" + selection_names: + - "{{ backup_selection_name }}" + register: _result_backup_selection_info + + - name: Verify result + ansible.builtin.assert: + that: + - _result_backup_selection_info.backup_selections | length == 1 + + - name: Modify an AWS Backup selection (check_mode) + amazon.aws.backup_selection: + selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + iam_role_arn: "{{ iam_role.iam_role.arn }}" + list_of_tags: + - condition_type: "STRINGEQUALS" + condition_key: "backup" + condition_value: "weekly" + check_mode: true + register: _modify_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - _modify_result_backup_selection.changed + + - name: Modify an AWS Backup selection + amazon.aws.backup_selection: + selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + iam_role_arn: "{{ iam_role.iam_role.arn }}" + list_of_tags: + - condition_type: "STRINGEQUALS" + condition_key: "backup" + condition_value: "weekly" + register: _modify_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - _modify_result_backup_selection.changed + - "'backup_selection' in _modify_result_backup_selection" + - _modify_result_backup_selection.backup_selection.iam_role_arn == iam_role.iam_role.arn + - _modify_result_backup_selection.backup_selection.selection_name == "{{ backup_selection_name }}" + + - name: Modify an AWS Backup selection (idempotency) + amazon.aws.backup_selection: + selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + iam_role_arn: "{{ iam_role.iam_role.arn }}" + list_of_tags: + - condition_type: "STRINGEQUALS" + condition_key: "backup" + condition_value: "weekly" + register: _modify_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - not _modify_result_backup_selection.changed + - "'backup_selection' in _modify_result_backup_selection" + - _modify_result_backup_selection.backup_selection.iam_role_arn == iam_role.iam_role.arn + - _modify_result_backup_selection.backup_selection.selection_name == "{{ backup_selection_name }}" + + - name: List all AWS Backup selections + amazon.aws.backup_selection_info: + backup_plan_name: "{{ backup_plan_name }}" + register: _result_backup_selection_list + + - name: Verify result + ansible.builtin.assert: + that: + - "'backup_selections' in _result_backup_selection_list" + - _result_backup_selection_list.backup_selections | length != 0 + + - name: Delete AWS Backup selection (check_mode) + amazon.aws.backup_selection: + backup_selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + state: absent + check_mode: true + register: _delete_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - _delete_result_backup_selection.changed + - "'backup_selection' in _delete_result_backup_selection" + + - name: Delete AWS Backup selection + amazon.aws.backup_selection: + backup_selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + state: absent + register: _delete_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - _delete_result_backup_selection.changed + - "'backup_selection' in _delete_result_backup_selection" + + - name: Delete AWS Backup selection (idempotency) + amazon.aws.backup_selection: + backup_selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + state: absent + register: _delete_result_backup_selection + + - name: Verify result + ansible.builtin.assert: + that: + - not _delete_result_backup_selection.changed + - "'backup_selection' in _delete_result_backup_selection" + + always: + - name: Delete AWS Backup selection created during this test + amazon.aws.backup_selection: + backup_selection_name: "{{ backup_selection_name }}" + backup_plan_name: "{{ backup_plan_name }}" + state: absent + ignore_errors: true + + # - name: Delete AWS Backup plan created during this test + # amazon.aws.backup_plan: + # backup_plan_name: "{{ backup_plan_name }}" + # state: absent + # ignore_errors: true + + - name: Delete AWS Backup plan created during this test + command: aws backup delete-backup-plan --backup-plan-id "{{ backup_plan_id }}" + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + register: _result_create_backup_plan + + - name: Delete AWS Backup vault created during this test + amazon.aws.backup_vault: + backup_vault_name: "{{ backup_vault_name }}" + state: absent + ignore_errors: true From 73f3adc6775c4497ad9ffe177b8c67e27251fc7e Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Sun, 7 May 2023 22:19:54 +0200 Subject: [PATCH 03/10] Remove logging Signed-off-by: Alina Buzachis --- meta/runtime.yml | 2 - plugins/module_utils/backup.py | 6 - plugins/modules/backup_plan.py | 323 ------------------ plugins/modules/backup_plan_info.py | 139 -------- plugins/modules/backup_selection.py | 6 - tests/integration/targets/backup_plan/aliases | 3 - .../targets/backup_plan/defaults/main.yml | 4 - .../targets/backup_plan/tasks/main.yml | 54 --- .../targets/backup_selection/tasks/main.yml | 2 +- 9 files changed, 1 insertion(+), 538 deletions(-) delete mode 100644 plugins/modules/backup_plan.py delete mode 100644 plugins/modules/backup_plan_info.py delete mode 100644 tests/integration/targets/backup_plan/aliases delete mode 100644 tests/integration/targets/backup_plan/defaults/main.yml delete mode 100644 tests/integration/targets/backup_plan/tasks/main.yml diff --git a/meta/runtime.yml b/meta/runtime.yml index 0242673f7ec..b335e9e0876 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -7,8 +7,6 @@ action_groups: - aws_az_info - aws_caller_info - aws_s3 - - backup_plan - - backup_plan_info - backup_tag - backup_tag_info - backup_selection diff --git a/plugins/module_utils/backup.py b/plugins/module_utils/backup.py index 4d04ea2e3c8..4435fc79ba5 100644 --- a/plugins/module_utils/backup.py +++ b/plugins/module_utils/backup.py @@ -13,12 +13,6 @@ from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict -import logging - -logging.basicConfig( - filename="/tmp/file.log", level=logging.DEBUG, format="%(asctime)s:%(levelname)s:%(name)s:%(message)s" -) - def get_backup_resource_tags(module, backup_client): resource = module.params.get("resource") diff --git a/plugins/modules/backup_plan.py b/plugins/modules/backup_plan.py deleted file mode 100644 index 9ee4f8a07b3..00000000000 --- a/plugins/modules/backup_plan.py +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Contributors to the Ansible project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - - -DOCUMENTATION = r""" ---- -module: backup_plan -version_added: 6.0.0 -short_description: create, delete and modify AWS Backup plans -description: - - Manage AWS Backup plans. - - For more information see the AWS documentation for Backup plans U(https://docs.aws.amazon.com/aws-backup/latest/devguide/about-backup-plans.html). -author: - - Kristof Imre Szabo (@krisek) - - Alina Buzachis (@alinabuzachis) -options: - backup_plan_name: - description: - - The display name of a backup plan. Must contain 1 to 50 alphanumeric or '-_.' characters. - required: true - type: str - aliases: ['name'] - rules: - description: - - An array of BackupRule objects, each of which specifies a scheduled task that is used to back up a selection of resources. - required: false - type: list - advanced_backup_settings: - description: - - Specifies a list of BackupOptions for each resource type. These settings are only available for Windows Volume Shadow Copy Service (VSS) backup jobs. - required: false - type: list - state: - description: - - Create, delete a backup plan. - required: false - default: present - choices: ['present', 'absent'] - type: str -extends_documentation_fragment: - - amazon.aws.common.modules - - amazon.aws.region.modules - - amazon.aws.boto3 - - amazon.aws.tags -""" - -EXAMPLES = r""" -- name: create backup plan - amazon.aws.backup_plan: - state: present - backup_plan_name: elastic - rules: - - RuleName: every_morning - TargetBackupVaultName: elastic - ScheduleExpression: "cron(0 5 ? * * *)" - StartWindowMinutes: 120 - CompletionWindowMinutes: 10080 - Lifecycle: - DeleteAfterDays: 7 - EnableContinuousBackup: true - -""" -RETURN = r""" -backup_plan_arn: - description: ARN of the backup plan. - type: str - sample: arn:aws:backup:eu-central-1:111122223333:backup-plan:1111f877-1ecf-4d79-9718-a861cd09df3b -backup_plan_id: - description: Id of the backup plan. - type: str - sample: 1111f877-1ecf-4d79-9718-a861cd09df3b -backup_plan_name: - description: Name of the backup plan. - type: str - sample: elastic -creation_date: - description: Creation date of the backup plan. - type: str - sample: '2023-01-24T10:08:03.193000+01:00' -last_execution_date: - description: Last execution date of the backup plan. - type: str - sample: '2023-03-24T06:30:08.250000+01:00' -tags: - description: Tags of the backup plan - type: str -version_id: - description: Version id of the backup plan - type: str -backup_plan: - description: backup plan details - returned: always - type: complex - contains: - backup_plan_arn: - description: backup plan arn - returned: always - type: str - sample: arn:aws:backup:eu-central-1:111122223333:backup-plan:1111f877-1ecf-4d79-9718-a861cd09df3b - backup_plan_name: - description: backup plan name - returned: always - type: str - sample: elastic - advanced_backup_settings: - description: Advanced backup settings of the backup plan - type: list - elements: dict - contains: - resource_type: - description: Resource type of the advanced setting - type: str - backup_options: - description: Options of the advanced setting - type: dict - rules: - description: - - An array of BackupRule objects, each of which specifies a scheduled task that is used to back up a selection of resources. - type: list - -""" - - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # Handled by AnsibleAWSModule - -import json -from typing import Optional -from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry -from ansible_collections.amazon.aws.plugins.module_utils.backup import get_plan_details - - -def create_backup_plan(module: AnsibleAWSModule, client, params: dict): - """ - Creates a Backup Plan - - module : AnsibleAWSModule object - client : boto3 backup client connection object - params : The parameters to create a backup plan - """ - params = {k: v for k, v in params.items() if v is not None} - try: - response = client.create_backup_plan(**params) - except ( - BotoCoreError, - ClientError, - ) as err: - module.fail_json_aws(err, msg="Failed to create Backup Plan") - - return response - - -def plan_update_needed(client, backup_plan_id: str, backup_plan_data: dict) -> bool: - update_needed = False - - # we need to get current rules to manage the plan - full_plan = client.get_backup_plan(BackupPlanId=backup_plan_id) - - configured_rules = json.dumps( - [ - {key: val for key, val in rule.items() if key != "RuleId"} - for rule in full_plan.get("BackupPlan", {}).get("Rules", []) - ], - sort_keys=True, - ) - supplied_rules = json.dumps(backup_plan_data["BackupPlan"]["Rules"], sort_keys=True) - - if configured_rules != supplied_rules: - # rules to be updated - update_needed = True - - configured_advanced_backup_settings = json.dumps( - full_plan.get("BackupPlan", {}).get("AdvancedBackupSettings", None), - sort_keys=True, - ) - supplied_advanced_backup_settings = json.dumps( - backup_plan_data["BackupPlan"]["AdvancedBackupSettings"], sort_keys=True - ) - if configured_advanced_backup_settings != supplied_advanced_backup_settings: - # advanced settings to be updated - update_needed = True - return update_needed - - -def update_backup_plan( - module: AnsibleAWSModule, client, backup_plan_id: str, backup_plan_data: dict -): - try: - response = client.update_backup_plan( - BackupPlanId=backup_plan_id, - BackupPlan=backup_plan_data["BackupPlan"], - ) - except ( - BotoCoreError, - ClientError, - ) as err: - module.fail_json_aws(err, msg="Failed to create Backup Plan") - return response - - -def delete_backup_plan(module: AnsibleAWSModule, client, backup_plan_id: str): - """ - Delete a Backup Plan - - module : AnsibleAWSModule object - client : boto3 client connection object - backup_plan_id : Backup Plan ID - """ - try: - client.delete_backup_plan(BackupPlanId=backup_plan_id) - except (BotoCoreError, ClientError) as err: - module.fail_json_aws(err, msg="Failed to delete the Backup Plan") - - -def main(): - argument_spec = dict( - state=dict(default="present", choices=["present", "absent"]), - backup_plan_name=dict(required=True, type="str"), - rules=dict(type="list"), - advanced_backup_settings=dict(default=[], type="list"), - creator_request_id=dict(type="str"), - tags=dict(required=False, type="dict", aliases=["resource_tags"]), - purge_tags=dict(default=True, type="bool"), - ) - - required_if = [ - ("state", "present", ["backup_plan_name", "rules"]), - ("state", "absent", ["backup_plan_name"]), - ] - - module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True) - - # collect parameters - state = module.params.get("state") - backup_plan_name = module.params["backup_plan_name"] - purge_tags = module.params["purge_tags"] - try: - client = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to connect to AWS") - - results = {"changed": False, "exists": False} - - current_plan = get_plan_details(module, client, backup_plan_name) - - if state == "present": - new_plan_data = { - "BackupPlan": { - "BackupPlanName": backup_plan_name, - "Rules": module.params["rules"], - "AdvancedBackupSettings": module.params.get("advanced_backup_settings"), - }, - "BackupPlanTags": module.params.get("tags"), - "CreatorRequestId": module.params.get("creator_request_id"), - } - - if not current_plan: # Plan does not exist, create it - results["exists"] = True - results["changed"] = True - - if module.check_mode: - module.exit_json(**results, msg="Would have created backup plan if not in check mode") - - create_backup_plan(module, client, new_plan_data) - - # TODO: add tags - # ensure_tags( - # client, - # module, - # response["BackupPlanArn"], - # purge_tags=module.params.get("purge_tags"), - # tags=module.params.get("tags"), - # resource_type="BackupPlan", - # ) - - else: # Plan exists, update if needed - results["exists"] = True - current_plan_id = current_plan[0]["backup_plan_id"] - if plan_update_needed(client, current_plan_id, new_plan_data): - results["changed"] = True - - if module.check_mode: - module.exit_json(**results, msg="Would have updated backup plan if not in check mode") - - update_backup_plan(module, client, current_plan_id, new_plan_data) - - if purge_tags: - pass - # TODO: Update plan tags - # ensure_tags( - # client, - # module, - # response["BackupPlanArn"], - # purge_tags=module.params.get("purge_tags"), - # tags=module.params.get("tags"), - # resource_type="BackupPlan", - # ) - - new_plan = get_plan_details(module, client, backup_plan_name) - results = results | new_plan[0] - - elif state == "absent": - if not current_plan: # Plan does not exist, can't delete it - module.exit_json(**results) - else: # Plan exists, delete it - results["changed"] = True - - if module.check_mode: - module.exit_json(**results, msg="Would have deleted backup plan if not in check mode") - - delete_backup_plan(module, client, current_plan[0]["backup_plan_id"]) - - module.exit_json(**results) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/backup_plan_info.py b/plugins/modules/backup_plan_info.py deleted file mode 100644 index 2fb5149ffa3..00000000000 --- a/plugins/modules/backup_plan_info.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - - -DOCUMENTATION = r""" ---- -module: backup_plan_info -version_added: 6.0.0 -short_description: Describe AWS Backup Plans -description: - - Lists info about Backup Plan configuration. -author: - - Gomathi Selvi Srinivasan (@GomathiselviS) - - Kristof Imre Szabo (@krisek) - - Alina Buzachis (@alinabuzachis) -options: - backup_plan_names: - type: list - elements: str - default: [] - description: - - Specifies a list of plan names. - - If an empty list is specified, information for the backup plans in the current region is returned. -extends_documentation_fragment: - - amazon.aws.common.modules - - amazon.aws.region.modules - - amazon.aws.boto3 -""" - -EXAMPLES = r""" -# Note: These examples do not set authentication details, see the AWS Guide for details. -# Gather information about all backup plans -- amazon.aws.backup_plan_info -# Gather information about a particular backup plan -- amazon.aws.backup_plan_info: - backup plan_names: - - elastic -""" - -RETURN = r""" -backup_plans: - description: List of backup plan objects. Each element consists of a dict with all the information related to that backup plan. - type: list - elements: dict - returned: always - contains: - backup_plan_arn: - description: ARN of the backup plan. - type: str - sample: arn:aws:backup:eu-central-1:111122223333:backup-plan:1111f877-1ecf-4d79-9718-a861cd09df3b - backup_plan_id: - description: Id of the backup plan. - type: str - sample: 1111f877-1ecf-4d79-9718-a861cd09df3b - backup_plan_name: - description: Name of the backup plan. - type: str - sample: elastic - creation_date: - description: Creation date of the backup plan. - type: str - sample: '2023-01-24T10:08:03.193000+01:00' - last_execution_date: - description: Last execution date of the backup plan. - type: str - sample: '2023-03-24T06:30:08.250000+01:00' - tags: - description: Tags of the backup plan - type: str - version_id: - description: Version id of the backup plan - type: str - backup_plan: - elements: dict - returned: always - description: Detailed information about the backup plan. - contains: - backup_plan_name: - description: Name of the backup plan. - type: str - sample: elastic - advanced_backup_settings: - description: Advanced backup settings of the backup plan - type: list - elements: dict - contains: - resource_type: - description: Resource type of the advanced setting - type: str - backup_options: - description: Options of the advanced setting - type: dict - rules: - description: - - An array of BackupRule objects, each of which specifies a scheduled task that is used to back up a selection of resources. - type: list -""" - -try: - import botocore -except ImportError: - pass # Handled by AnsibleAWSModule - - -from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry -from ansible_collections.amazon.aws.plugins.module_utils.backup import get_plan_details - - -def get_backup_plan_detail(client, module): - backup_plan_list = [] - backup_plan_names = module.params.get("backup_plan_names") - - for name in backup_plan_names: - backup_plan_list.extend(get_plan_details(module, client, name)) - - module.exit_json(**{"backup_plans": backup_plan_list}) - - -def main(): - argument_spec = dict( - backup_plan_names=dict(type="list", elements="str", default=[]), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - try: - connection = module.client("backup", retry_decorator=AWSRetry.jittered_backoff()) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to connect to AWS") - - get_backup_plan_detail(connection, module) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/backup_selection.py b/plugins/modules/backup_selection.py index b673e758a35..5cf16713c4f 100644 --- a/plugins/modules/backup_selection.py +++ b/plugins/modules/backup_selection.py @@ -152,12 +152,6 @@ from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -import logging - -logging.basicConfig( - filename="/tmp/file.log", level=logging.DEBUG, format="%(asctime)s:%(levelname)s:%(name)s:%(message)s" -) - def check_for_update(current_selection, backup_selection_data, iam_role_arn): update_needed = False diff --git a/tests/integration/targets/backup_plan/aliases b/tests/integration/targets/backup_plan/aliases deleted file mode 100644 index d9b03076320..00000000000 --- a/tests/integration/targets/backup_plan/aliases +++ /dev/null @@ -1,3 +0,0 @@ -cloud/aws -backup_plan -backup_vault diff --git a/tests/integration/targets/backup_plan/defaults/main.yml b/tests/integration/targets/backup_plan/defaults/main.yml deleted file mode 100644 index 35af3bc6551..00000000000 --- a/tests/integration/targets/backup_plan/defaults/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -# defaults file for test_backup_plan -backup_vault_name: '{{ tiny_prefix }}-backup-vault' -backup_plan_name: '{{ tiny_prefix }}-backup-plan' diff --git a/tests/integration/targets/backup_plan/tasks/main.yml b/tests/integration/targets/backup_plan/tasks/main.yml deleted file mode 100644 index 45cfa380611..00000000000 --- a/tests/integration/targets/backup_plan/tasks/main.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -- module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region }}" - block: - - name: Create an AWS Backup vault for the plan to target - amazon.aws.backup_vault: - backup_vault_name: "{{ backup_vault_name }}" - - - name: Create an AWS Backup plan - amazon.aws.backup_plan: - backup_plan_name: "{{ backup_plan_name }}" - rules: - - RuleName: daily - TargetBackupVaultName: "{{ backup_vault_name }}" - ScheduleExpression: "cron(0 5 ? * * *)" - StartWindowMinutes: 60 - CompletionWindowMinutes: 1440 - tags: - environment: test - register: backup_plan_create_result - - - name: Print result - ansible.builtin.debug: - msg: "{{ backup_plan_create_result }}" - - - name: Verify results - ansible.builtin.assert: - that: - - backup_plan_create_result.exists - - backup_plan_create_result.changed - - backup_plan_create_result.backup_plan.backup_plan_name == backup_plan_name - - - name: Get info on AWS Backup plan - amazon.aws.backup_plan_info: - backup_plan_names: - - "{{ backup_plan_name }}" - register: backup_plan_info_result - - always: - - name: Delete AWS Backup plan created during this test - amazon.aws.backup_plan: - backup_plan_name: "{{ backup_plan_name }}" - state: absent - ignore_errors: true - - - name: Delete AWS Backup vault created during this test - amazon.aws.backup_vault: - backup_vault_name: "{{ backup_vault_name }}" - state: absent - ignore_errors: true diff --git a/tests/integration/targets/backup_selection/tasks/main.yml b/tests/integration/targets/backup_selection/tasks/main.yml index db52d563f54..bbd5a38bb40 100644 --- a/tests/integration/targets/backup_selection/tasks/main.yml +++ b/tests/integration/targets/backup_selection/tasks/main.yml @@ -262,7 +262,7 @@ AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" AWS_SESSION_TOKEN: "{{ security_token | default('') }}" AWS_DEFAULT_REGION: "{{ aws_region }}" - register: _result_create_backup_plan + ignore_errors: true - name: Delete AWS Backup vault created during this test amazon.aws.backup_vault: From 6aa6d51169e1affcf4510b066f4fe0dc12aebf1e Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Mon, 8 May 2023 01:29:59 +0200 Subject: [PATCH 04/10] Fix docs Signed-off-by: Alina Buzachis --- plugins/modules/backup_selection.py | 13 ++++++------- plugins/modules/backup_selection_info.py | 10 +++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/plugins/modules/backup_selection.py b/plugins/modules/backup_selection.py index 5cf16713c4f..5db6786ab2f 100644 --- a/plugins/modules/backup_selection.py +++ b/plugins/modules/backup_selection.py @@ -21,7 +21,7 @@ type: str aliases: - plan_name - selection_name: + backup_selection_name: description: - The display name of a resource selection document. Must contain 1 to 50 alphanumeric or '-_.' characters. required: true @@ -62,8 +62,7 @@ default: present choices: ['present', 'absent'] type: str - -authors: +author: - Kristof Imre Szabo (@krisek) - Alina Buzachis (@alinabuzachis) extends_documentation_fragment: @@ -94,22 +93,22 @@ description: Backup plan id. returned: always type: str - sample: 1111f877-1ecf-4d79-9718-a861cd09df3b + sample: "1111f877-1ecf-4d79-9718-a861cd09df3b" creation_date: description: Backup plan creation date. returned: always type: str - sample: 2023-01-24T10:08:03.193000+01:00 + sample: "2023-01-24T10:08:03.193000+01:00" iam_role_arn: description: The ARN of the IAM role that Backup uses. returned: always type: str - sample: arn:aws:iam::111122223333:role/system-backup + sample: "arn:aws:iam::111122223333:role/system-backup" selection_id: description: Backup selection id. returned: always type: str - sample: 1111c217-5d71-4a55-8728-5fc4e63d437b + sample: "1111c217-5d71-4a55-8728-5fc4e63d437b" selection_name: description: Backup selection name. returned: always diff --git a/plugins/modules/backup_selection_info.py b/plugins/modules/backup_selection_info.py index 0654810b1bc..70ba643ee77 100644 --- a/plugins/modules/backup_selection_info.py +++ b/plugins/modules/backup_selection_info.py @@ -28,7 +28,7 @@ description: - Uniquely identifies the backup plan the selections should be listed for. type: list - elemenst: str + elements: str aliases: - selection_names extends_documentation_fragment: @@ -61,22 +61,22 @@ description: Backup plan id. returned: always type: str - sample: 1111f877-1ecf-4d79-9718-a861cd09df3b + sample: "1111f877-1ecf-4d79-9718-a861cd09df3b" creation_date: description: Backup plan creation date. returned: always type: str - sample: 2023-01-24T10:08:03.193000+01:00 + sample: "2023-01-24T10:08:03.193000+01:00" iam_role_arn: description: IAM role arn. returned: always type: str - sample: arn:aws:iam::111122223333:role/system-backup + sample: "arn:aws:iam::111122223333:role/system-backup" selection_id: description: Backup selection id. returned: always type: str - sample: 1111c217-5d71-4a55-8728-5fc4e63d437b + sample: "1111c217-5d71-4a55-8728-5fc4e63d437b" selection_name: description: Backup selection name. returned: always From fff8a7be385b34c14bafb01614edb31ffae8a791 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Mon, 8 May 2023 01:38:27 +0200 Subject: [PATCH 05/10] UPdate meta/runtime.yml Signed-off-by: Alina Buzachis --- tests/integration/targets/backup_selection/aliases | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/targets/backup_selection/aliases b/tests/integration/targets/backup_selection/aliases index e966bd6141c..190ba4c8e94 100644 --- a/tests/integration/targets/backup_selection/aliases +++ b/tests/integration/targets/backup_selection/aliases @@ -3,4 +3,3 @@ cloud/aws backup_selection backup_selection_info backup_vault -backup_plan From f1590e7c956b6e48e3dbe6cdf63be90ed723bd27 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Mon, 8 May 2023 17:14:19 +0200 Subject: [PATCH 06/10] Fix backup Signed-off-by: Alina Buzachis --- plugins/module_utils/backup.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/backup.py b/plugins/module_utils/backup.py index 4435fc79ba5..180927db515 100644 --- a/plugins/module_utils/backup.py +++ b/plugins/module_utils/backup.py @@ -68,8 +68,8 @@ def get_plan_details(module, client, backup_plan_name: str): try: module.params["resource"] = result.get("BackupPlanArn", None) - tag_dict = get_backup_resource_tags(module, client) - result.update({"tags": tag_dict}) + # tag_dict = get_backup_resource_tags(module, client) + # result.update({"tags": tag_dict}) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_aws(e, msg="Failed to get the backup plan tags") @@ -77,9 +77,8 @@ def get_plan_details(module, client, backup_plan_name: str): # Turn the boto3 result in to ansible friendly tag dictionary for v in snaked_backup_plan: - if "tags_list" in v: - v["tags"] = boto3_tag_list_to_ansible_dict(v["tags_list"], "key", "value") - del v["tags_list"] + # if "tags_list" in v: + # v["tags"] = boto3_tag_list_to_ansible_dict(v["tags_list"], "key", "value") if "response_metadata" in v: del v["response_metadata"] v["backup_plan_name"] = v["backup_plan"]["backup_plan_name"] From 2a5c187bacb3561c527a3323f655f0931f16d63e Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Mon, 8 May 2023 17:30:00 +0200 Subject: [PATCH 07/10] Add changelog Signed-off-by: Alina Buzachis --- changelogs/fragments/backup_add_backup_selections_logic.yml | 2 ++ plugins/module_utils/backup.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/backup_add_backup_selections_logic.yml diff --git a/changelogs/fragments/backup_add_backup_selections_logic.yml b/changelogs/fragments/backup_add_backup_selections_logic.yml new file mode 100644 index 00000000000..291e2f94b3d --- /dev/null +++ b/changelogs/fragments/backup_add_backup_selections_logic.yml @@ -0,0 +1,2 @@ +minor_changes: + - backup - Add logic for backup_selection* modules (https://github.com/ansible-collections/amazon.aws/pull/1530). diff --git a/plugins/module_utils/backup.py b/plugins/module_utils/backup.py index 180927db515..8f7ac265082 100644 --- a/plugins/module_utils/backup.py +++ b/plugins/module_utils/backup.py @@ -11,7 +11,6 @@ from typing import Union from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict def get_backup_resource_tags(module, backup_client): From af34b126c76cd867e439c2bec53b48a8ba6c04a5 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Mon, 8 May 2023 20:19:18 +0200 Subject: [PATCH 08/10] Add pause Signed-off-by: Alina Buzachis --- tests/integration/targets/backup_selection/tasks/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/targets/backup_selection/tasks/main.yml b/tests/integration/targets/backup_selection/tasks/main.yml index bbd5a38bb40..f9aeedcc4a8 100644 --- a/tests/integration/targets/backup_selection/tasks/main.yml +++ b/tests/integration/targets/backup_selection/tasks/main.yml @@ -17,6 +17,10 @@ - "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup" register: iam_role + # Wait for the role to be created + - pause: + seconds: 20 + - name: Create an AWS Backup vault for the plan to target amazon.aws.backup_vault: backup_vault_name: "{{ backup_vault_name }}" From 3a75d1c45726469d5454f4d8c1377339681a413d Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Mon, 8 May 2023 21:55:50 +0200 Subject: [PATCH 09/10] Apply review Signed-off-by: Alina Buzachis --- plugins/modules/backup_selection.py | 23 ++++++++++++++++++- plugins/modules/backup_selection_info.py | 3 --- .../targets/backup_selection/tasks/main.yml | 3 ++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/plugins/modules/backup_selection.py b/plugins/modules/backup_selection.py index 5db6786ab2f..c2f8d85cbb9 100644 --- a/plugins/modules/backup_selection.py +++ b/plugins/modules/backup_selection.py @@ -45,6 +45,20 @@ - Condition operators are case sensitive. type: list elements: dict + suboptions: + condition_type: + description: + - An operation applied to a key-value pair used to assign resources to your backup plan. + - Condition only supports C(StringEquals). + type: str + condition_key: + description: + - The key in a key-value pair. + type: str + condition_value: + description: + - The value in a key-value pair. + type: str not_resources: description: - A list of Amazon Resource Names (ARNs) to exclude from a backup plan. The maximum number of ARNs is 500 without wildcards, @@ -55,6 +69,7 @@ conditions: description: - A list of conditions (expressed as a dict) that you define to assign resources to your backup plans using tags. + - I(conditions) supports C(StringEquals), C(StringLike), C(StringNotEquals), and C(StringNotLike). I(list_of_tags) only supports C(StringEquals). type: dict state: description: @@ -207,7 +222,13 @@ def main(): resources=dict(type="list", elements="str"), conditions=dict(type="dict"), not_resources=dict(type="list", elements="str"), - list_of_tags=dict(type="list", elements="dict"), + list_of_tags=dict( + type="list", + elements="dict", + options=dict( + condition_type=dict(type="str"), condition_key=dict(type="str"), condition_value=dict(type="str") + ), + ), state=dict(default="present", choices=["present", "absent"]), ) required_if = [ diff --git a/plugins/modules/backup_selection_info.py b/plugins/modules/backup_selection_info.py index 70ba643ee77..dcb8f6571a2 100644 --- a/plugins/modules/backup_selection_info.py +++ b/plugins/modules/backup_selection_info.py @@ -121,9 +121,6 @@ def main(): backup_plan_name=dict(type="str", required=True, aliases=["plan_name"]), backup_selection_names=dict(type="list", elements="str", aliases=["selection_names"]), ) - - global client - global module result = {} module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) diff --git a/tests/integration/targets/backup_selection/tasks/main.yml b/tests/integration/targets/backup_selection/tasks/main.yml index f9aeedcc4a8..f561b3ad96d 100644 --- a/tests/integration/targets/backup_selection/tasks/main.yml +++ b/tests/integration/targets/backup_selection/tasks/main.yml @@ -15,11 +15,12 @@ description: "Ansible AWS Backup Role" managed_policy: - "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup" + wait: true register: iam_role # Wait for the role to be created - pause: - seconds: 20 + seconds: 5 - name: Create an AWS Backup vault for the plan to target amazon.aws.backup_vault: From 8dea8eafcd1902af2cf54d139564395c282afe14 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Mon, 8 May 2023 22:20:46 +0200 Subject: [PATCH 10/10] Add no_log: False Signed-off-by: Alina Buzachis --- plugins/modules/backup_selection.py | 4 +++- tests/integration/targets/backup_selection/tasks/main.yml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/backup_selection.py b/plugins/modules/backup_selection.py index c2f8d85cbb9..e6edc251a31 100644 --- a/plugins/modules/backup_selection.py +++ b/plugins/modules/backup_selection.py @@ -226,7 +226,9 @@ def main(): type="list", elements="dict", options=dict( - condition_type=dict(type="str"), condition_key=dict(type="str"), condition_value=dict(type="str") + condition_type=dict(type="str"), + condition_key=dict(type="str", no_log=False), + condition_value=dict(type="str"), ), ), state=dict(default="present", choices=["present", "absent"]), diff --git a/tests/integration/targets/backup_selection/tasks/main.yml b/tests/integration/targets/backup_selection/tasks/main.yml index f561b3ad96d..98ac62bc2c5 100644 --- a/tests/integration/targets/backup_selection/tasks/main.yml +++ b/tests/integration/targets/backup_selection/tasks/main.yml @@ -15,7 +15,7 @@ description: "Ansible AWS Backup Role" managed_policy: - "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup" - wait: true + wait: true register: iam_role # Wait for the role to be created