diff --git a/changelogs/fragments/1705-rds_cluster-add-support-remove-cluster-from-global-db.yml b/changelogs/fragments/1705-rds_cluster-add-support-remove-cluster-from-global-db.yml new file mode 100644 index 00000000000..ce9b3df2295 --- /dev/null +++ b/changelogs/fragments/1705-rds_cluster-add-support-remove-cluster-from-global-db.yml @@ -0,0 +1,3 @@ +--- +minor_changes: +- rds_cluster - Add support for removing cluster from global db (https://github.com/ansible-collections/amazon.aws/pull/1705). diff --git a/plugins/module_utils/rds.py b/plugins/module_utils/rds.py index d4fb5380748..5141b5ad62d 100644 --- a/plugins/module_utils/rds.py +++ b/plugins/module_utils/rds.py @@ -123,6 +123,8 @@ def get_rds_method_attribute(method_name, module): waiter = "role_disassociated" elif method_name == "promote_read_replica": waiter = "read_replica_promoted" + elif method_name == "db_cluster_promoting": + waiter = "db_cluster_promoting" else: waiter = "db_instance_available" # Handle retry codes diff --git a/plugins/module_utils/waiters.py b/plugins/module_utils/waiters.py index f8a3b69c117..81c9789617e 100644 --- a/plugins/module_utils/waiters.py +++ b/plugins/module_utils/waiters.py @@ -547,6 +547,19 @@ rds_data = { "version": 2, "waiters": { + "DBClusterPromoting": { + "delay": 5, + "maxAttempts": 60, + "operation": "DescribeDBClusters", + "acceptors": [ + { + "state": "success", + "matcher": "pathAll", + "argument": "DBClusters[].Status", + "expected": "promoting", + }, + ], + }, "DBInstanceStopped": { "delay": 20, "maxAttempts": 60, @@ -911,6 +924,11 @@ def route53_model(name): elbv2_model("LoadBalancersDeleted"), core_waiter.NormalizedOperationMethod(elbv2.describe_load_balancers), ), + ("RDS", "db_cluster_promoting"): lambda rds: core_waiter.Waiter( + "db_cluster_promoting", + rds_model("DBClusterPromoting"), + core_waiter.NormalizedOperationMethod(rds.describe_db_clusters), + ), ("RDS", "db_instance_stopped"): lambda rds: core_waiter.Waiter( "db_instance_stopped", rds_model("DBInstanceStopped"), diff --git a/plugins/modules/rds_cluster.py b/plugins/modules/rds_cluster.py index 187bfbe28e6..7378bd86d48 100644 --- a/plugins/modules/rds_cluster.py +++ b/plugins/modules/rds_cluster.py @@ -305,6 +305,13 @@ aliases: - maintenance_window type: str + remove_from_global_db: + description: + - If set to C(true), the cluster will be removed from global DB. + - Parameters I(global_cluster_identifier), I(db_cluster_identifier) must be specified when I(remove_from_global_db=true). + type: bool + required: False + version_added: 6.5.0 replication_source_identifier: description: - The Amazon Resource Name (ARN) of the source DB instance or DB cluster if this DB cluster is created as a Read Replica. @@ -463,6 +470,42 @@ engine: aurora-postgresql state: present db_instance_class: 'db.t3.medium' + +- name: Remove a cluster from global DB (do not delete) + amazon.aws.rds_cluster: + db_cluster_identifier: '{{ cluster_id }}' + global_cluster_identifier: '{{ global_cluster_id }}' + remove_from_global_db: true + +- name: Remove a cluster from global DB and Delete without creating a final snapshot + amazon.aws.rds_cluster: + engine: aurora + password: "{{ password }}" + username: "{{ username }}" + cluster_id: "{{ cluster_id }}" + skip_final_snapshot: true + remove_from_global_db: true + wait: true + state: absent + +- name: Update cluster port and WAIT for remove secondary DB cluster from global DB to complete + amazon.aws.rds_cluster: + db_cluster_identifier: "{{ secondary_cluster_name }}" + global_cluster_identifier: "{{ global_cluster_name }}" + remove_from_global_db: true + state: present + port: 3389 + region: "{{ secondary_cluster_region }}" + +- name: Update cluster port and DO NOT WAIT for remove secondary DB cluster from global DB to complete + amazon.aws.rds_cluster: + db_cluster_identifier: "{{ secondary_cluster_name }}" + global_cluster_identifier: "{{ global_cluster_name }}" + remove_from_global_db: true + state: present + port: 3389 + region: "{{ secondary_cluster_region }}" + wait: false """ RETURN = r""" @@ -1102,6 +1145,33 @@ def ensure_present(cluster, parameters, method_name, method_options_name): return changed +def handle_remove_from_global_db(module, cluster): + global_cluster_id = module.params.get("global_cluster_identifier") + db_cluster_id = module.params.get("db_cluster_identifier") + db_cluster_arn = cluster["DBClusterArn"] + + if module.check_mode: + return True + + try: + client.remove_from_global_cluster(DbClusterIdentifier=db_cluster_arn, GlobalClusterIdentifier=global_cluster_id) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws( + e, msg=f"Failed to remove cluster {db_cluster_id} from global DB cluster {global_cluster_id}." + ) + + # for replica cluster - wait for cluster to change status from 'available' to 'promoting' + # only replica/secondary clusters have "GlobalWriteForwardingStatus" field + if "GlobalWriteForwardingStatus" in cluster: + wait_for_cluster_status(client, module, db_cluster_id, "db_cluster_promoting") + + # if wait=true, wait for db cluster remove from global db operation to complete + if module.params.get("wait"): + wait_for_cluster_status(client, module, db_cluster_id, "cluster_available") + + return True + + def main(): global module global client @@ -1154,6 +1224,7 @@ def main(): port=dict(type="int"), preferred_backup_window=dict(aliases=["backup_window"]), preferred_maintenance_window=dict(aliases=["maintenance_window"]), + remove_from_global_db=dict(type="bool"), replication_source_identifier=dict(aliases=["replication_src_id"]), restore_to_time=dict(), restore_type=dict(choices=["full-copy", "copy-on-write"]), @@ -1190,6 +1261,7 @@ def main(): required_if=[ ["creation_source", "snapshot", ["snapshot_identifier", "engine"]], ["creation_source", "s3", required_by_s3_creation_source], + ["remove_from_global_db", True, ["global_cluster_identifier", "db_cluster_identifier"]], ], mutually_exclusive=[ ["s3_bucket_name", "source_db_cluster_identifier", "snapshot_identifier"], @@ -1245,12 +1317,17 @@ def main(): msg="skip_final_snapshot is False but all of the following are missing: final_snapshot_identifier" ) - parameters = arg_spec_to_rds_params(dict((k, module.params[k]) for k in module.params if k in parameter_options)) changed = False + + parameters = arg_spec_to_rds_params(dict((k, module.params[k]) for k in module.params if k in parameter_options)) method_name, method_options_name = get_rds_method_attribute_name(cluster) if method_name: if method_name == "delete_db_cluster": + if cluster and module.params.get("remove_from_global_db"): + if cluster["Engine"] in ["aurora", "aurora-mysql", "aurora-postgresql"]: + changed = handle_remove_from_global_db(module, cluster) + call_method(client, module, method_name, eval(method_options_name)(parameters)) changed = True else: @@ -1261,6 +1338,12 @@ def main(): else: cluster_id = module.params["db_cluster_identifier"] + if cluster_id and get_cluster(cluster_id) and module.params.get("remove_from_global_db"): + if cluster["Engine"] in ["aurora", "aurora-mysql", "aurora-postgresql"]: + if changed: + wait_for_cluster_status(client, module, cluster_id, "cluster_available") + changed |= handle_remove_from_global_db(module, cluster) + result = camel_dict_to_snake_dict(get_cluster(cluster_id)) if result: diff --git a/tests/integration/requirements.yml b/tests/integration/requirements.yml index df4d6171dc1..fa69c3272a8 100644 --- a/tests/integration/requirements.yml +++ b/tests/integration/requirements.yml @@ -2,3 +2,4 @@ collections: - ansible.windows - ansible.utils # ipv6 filter +- amazon.cloud # used by integration tests - rds_cluster_modify diff --git a/tests/integration/targets/rds_cluster_modify/defaults/main.yml b/tests/integration/targets/rds_cluster_modify/defaults/main.yml index 073f7a4c000..e52c833c84f 100644 --- a/tests/integration/targets/rds_cluster_modify/defaults/main.yml +++ b/tests/integration/targets/rds_cluster_modify/defaults/main.yml @@ -12,3 +12,20 @@ new_cluster_id: ansible-test-cluster-{{ tiny_prefix }}-new new_port: 1155 new_password: test-rds_password-new new_db_parameter_group_name: ansible-test-db-parameter-group-{{ tiny_prefix }}-new + +test_engine: aurora-mysql +test_engine_version: 8.0 +test_instance_class: db.r5.large + +# Global cluster parameters ================================ +test_global_cluster_name: ansible-test-global-{{ tiny_prefix }} + +# Primary cluster parameters ================================ +test_primary_cluster_name: ansible-test-primary-{{ tiny_prefix }} +test_primary_cluster_region: us-west-2 +test_primary_cluster_instance_name: ansible-test-instance-primary-{{ tiny_prefix }} + +# Replica cluster parameters ================================ +test_replica_cluster_name: ansible-test-replica-{{ tiny_prefix }} +test_replica_cluster_region: eu-north-1 +test_replica_cluster_instance_name: ansible-test-instance-replica-{{ tiny_prefix }} diff --git a/tests/integration/targets/rds_cluster_modify/tasks/main.yaml b/tests/integration/targets/rds_cluster_modify/tasks/main.yaml index 31a20b9a4da..d7118807fe8 100644 --- a/tests/integration/targets/rds_cluster_modify/tasks/main.yaml +++ b/tests/integration/targets/rds_cluster_modify/tasks/main.yaml @@ -5,6 +5,12 @@ secret_key: '{{ aws_secret_key }}' session_token: '{{ security_token | default(omit) }}' block: + + # Disabled: Below tests require use of more than 1 region, not supported by CI at the moment + # Tests have been ran, tested, and verified locally on us-west-2 (primary), eu-north-1 (replica) + # - name: Run tests for testing remove cluster from global db + # import_tasks: remove_from_global_db.yaml + - name: Ensure the resource doesn't exist rds_cluster: id: '{{ cluster_id }}' @@ -177,7 +183,7 @@ - name: Create DB cluster parameter group if not exists command: aws rds create-db-cluster-parameter-group --db-cluster-parameter-group-name - {{ new_db_parameter_group_name }} --db-parameter-group-family aurora-mysql5.7 --description + {{ new_db_parameter_group_name }} --db-parameter-group-family aurora-mysql8.0 --description "Test DB cluster parameter group" environment: AWS_ACCESS_KEY_ID: '{{ aws_access_key }}' diff --git a/tests/integration/targets/rds_cluster_modify/tasks/remove_from_global_db.yaml b/tests/integration/targets/rds_cluster_modify/tasks/remove_from_global_db.yaml new file mode 100644 index 00000000000..984c14ff381 --- /dev/null +++ b/tests/integration/targets/rds_cluster_modify/tasks/remove_from_global_db.yaml @@ -0,0 +1,244 @@ +--- +- name: Run tests for testing remove cluster from global db + block: + + # Create global db ------------------------------------------------------------------------------- + + - name: Create rds global database + amazon.cloud.rds_global_cluster: + global_cluster_identifier: "{{ test_global_cluster_name }}" + engine: "{{ test_engine }}" + engine_version: "{{ test_engine_version }}" + region: "{{ test_primary_cluster_region }}" + state: present + register: create_global_result + + # Create primary cluster with an instance --------------------------------------------------------------- + + - name: Create a primary cluster for global database + amazon.aws.rds_cluster: + global_cluster_identifier: "{{ test_global_cluster_name }}" + db_cluster_identifier: "{{ test_primary_cluster_name }}" + region: "{{ test_primary_cluster_region }}" + engine: "{{ test_engine }}" + engine_version: "{{ test_engine_version }}" + username: "{{ username }}" + password: "{{ password }}" + register: create_primary_result + + - name: Create an instance connected to primary cluster + amazon.aws.rds_instance: + db_cluster_identifier: "{{ test_primary_cluster_name }}" + db_instance_identifier: "{{ test_primary_cluster_name }}-instance" + region: "{{ test_primary_cluster_region }}" + engine: "{{ test_engine }}" + db_instance_class: "{{ test_instance_class }}" + + - name: Get primary cluster info + amazon.aws.rds_cluster_info: + db_cluster_identifier: "{{ test_primary_cluster_name }}" + region: "{{ test_primary_cluster_region }}" + register: primary_cluster_info_result + + - name: Get global db info + command: 'aws rds describe-global-clusters --global-cluster-identifier {{ test_global_cluster_name }}' + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ test_primary_cluster_region }}" + register: global_cluster_info_result + + - name: convert it to an object + set_fact: + global_cluster_info: "{{ global_cluster_info_result.stdout | from_json }}" + + - name: Assert that primary cluster is a part of global db + assert: + that: + - global_cluster_info.GlobalClusters[0].GlobalClusterMembers[0].DBClusterArn == primary_cluster_info_result.clusters[0].db_cluster_arn + + # Create replica cluster ------------------------------------------------------------------------------- + + - name: Create a replica cluster for global database + amazon.aws.rds_cluster: + global_cluster_identifier: "{{ test_global_cluster_name }}" + db_cluster_identifier: "{{ test_replica_cluster_name }}" + engine: "{{ test_engine }}" + engine_version: "{{ global_cluster_info.GlobalClusters[0].EngineVersion }}" # replica cluster engine version needs to be exact same as global db engine version + region: "{{ test_replica_cluster_region }}" + register: create_replica_result + + - name: Get replica cluster info + amazon.aws.rds_cluster_info: + db_cluster_identifier: "{{ test_replica_cluster_name }}" + region: "{{ test_replica_cluster_region }}" + register: replica_cluster_info_result + + - name: Get global db info + command: 'aws rds describe-global-clusters --global-cluster-identifier {{ test_global_cluster_name }}' + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ test_primary_cluster_region }}" + register: global_cluster_info_result + + - name: convert it to an object + set_fact: + global_cluster_info: "{{ global_cluster_info_result.stdout | from_json }}" + + - name: Assert that replica cluster is a part of global db + assert: + that: + - global_cluster_info.GlobalClusters[0].GlobalClusterMembers[1].DBClusterArn == replica_cluster_info_result.clusters[0].db_cluster_arn + + # Test delete on replica cluster part of global db---------------------------------------------------------------- + + - name: Delete DB cluster without final snapshot (fails as its a part of global db) + amazon.aws.rds_cluster: + db_cluster_identifier: "{{ test_replica_cluster_name }}" + global_cluster_identifier: "{{ test_global_cluster_name }}" + region: "{{ test_replica_cluster_region }}" + skip_final_snapshot: true + state: absent + register: delete_replica_cluster_result + ignore_errors: true + + - name: Assert that deletion failed due to cluster being part of global db + assert: + that: + - delete_replica_cluster_result is failed + - delete_replica_cluster_result is not changed + - '"is a part of a global cluster, please remove it from global cluster" in delete_replica_cluster_result.error.message' + + # Test modify replica DB cluster along with removing it from global db------------------------------------------------ + + - name: Remove replica DB cluster from global DB and modify cluster port + amazon.aws.rds_cluster: + db_cluster_identifier: "{{ test_replica_cluster_name }}" + global_cluster_identifier: "{{ test_global_cluster_name }}" + remove_from_global_db: true + state: present + port: 3389 + region: "{{ test_replica_cluster_region }}" + register: modify_port_result + + - name: Get replica cluster info + amazon.aws.rds_cluster_info: + db_cluster_identifier: "{{ test_replica_cluster_name }}" + region: "{{ test_replica_cluster_region }}" + register: replica_cluster_info_result + + - assert: + that: + - modify_port_result is not failed + - modify_port_result is changed + - replica_cluster_info_result.clusters[0].port == 3389 + + - name: Get global db info + command: 'aws rds describe-global-clusters --global-cluster-identifier {{ test_global_cluster_name }}' + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ test_primary_cluster_region }}" + register: global_cluster_info_result + + - name: convert it to an object + set_fact: + global_cluster_info: "{{ global_cluster_info_result.stdout | from_json }}" + + - name: Assert that replica cluster is NOT a part of global db + assert: + that: + - global_cluster_info.GlobalClusters[0].GlobalClusterMembers | length == 1 + - global_cluster_info.GlobalClusters[0].GlobalClusterMembers[0].DBClusterArn != replica_cluster_info_result.clusters[0].db_cluster_arn + + # Test delete on replica cluster as NOT a part of global db---------------------------------------------------------------- + + - name: Delete replica cluster + amazon.aws.rds_cluster: + db_cluster_identifier: "{{ test_replica_cluster_name }}" + global_cluster_identifier: "{{ test_global_cluster_name }}" + region: "{{ test_replica_cluster_region }}" + skip_final_snapshot: true + state: absent + register: delete_replica_cluster_result + + - name: Assert that replica cluster deletion succeeded + assert: + that: + - delete_replica_cluster_result is not failed + - delete_replica_cluster_result is changed + + # Test remove primary cluster from global db------------------------------------------------------------ + - name: Remove primary cluster from global db + amazon.aws.rds_cluster: + global_cluster_identifier: '{{ test_global_cluster_name }}' + db_cluster_identifier: '{{ test_primary_cluster_name }}' + region: "{{ test_primary_cluster_region }}" + remove_from_global_db: true + + - name: Get global db info + command: 'aws rds describe-global-clusters --global-cluster-identifier {{ test_global_cluster_name }}' + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ test_primary_cluster_region }}" + register: global_cluster_info_result + + - name: convert it to an object + set_fact: + global_cluster_info: "{{ global_cluster_info_result.stdout | from_json }}" + + - name: Assert that primary cluster is NOT a part of global db + assert: + that: + - global_cluster_info.GlobalClusters[0].GlobalClusterMembers | length == 0 + + # Cleanup starts------------------------------------------------------------ + + always: + - name: Delete replica cluster + amazon.aws.rds_cluster: + db_cluster_identifier: "{{ test_replica_cluster_name }}" + global_cluster_identifier: "{{ test_global_cluster_name }}" + skip_final_snapshot: true + region: "{{ test_replica_cluster_region }}" + state: absent + ignore_errors: true + + - name: Delete instance connected to primary cluster + amazon.aws.rds_instance: + db_cluster_identifier: "{{ test_primary_cluster_name }}" + db_instance_identifier: "{{ test_primary_cluster_name }}-instance" + engine: "{{ test_engine }}" + db_instance_class: "{{ test_instance_class }}" + skip_final_snapshot: true + region: "{{ test_primary_cluster_region }}" + state: absent + ignore_errors: true + + - name: Delete primary cluster + amazon.aws.rds_cluster: + db_cluster_identifier: "{{ test_primary_cluster_name }}" + global_cluster_identifier: "{{ test_global_cluster_name }}" + engine: "{{ test_engine }}" + engine_version: "{{ test_engine_version }}" + username: "{{ username }}" + password: "{{ password }}" + skip_final_snapshot: true + region: "{{ test_primary_cluster_region }}" + state: absent + ignore_errors: true + + - name: Delete global db + amazon.cloud.rds_global_cluster: + global_cluster_identifier: "{{ test_global_cluster_name }}" + engine: "{{ test_engine }}" + engine_version: "{{ test_engine_version }}" + region: "{{ test_primary_cluster_region }}" + state: absent + ignore_errors: true