From 83dfcebd51d34c2577ab9f6f26d62d6d25bb2b45 Mon Sep 17 00:00:00 2001 From: Sebastien Rosset Date: Mon, 13 Jun 2022 12:28:23 -0700 Subject: [PATCH] Add 'opensearch' and 'opensearch_info' modules (#859) Add 'opensearch' and 'opensearch_info' modules SUMMARY Add opensearch module to create/update AWS OpenSearch/Elasticsearch domains. Add opensearch_info module to query AWS OpenSearch/Elasticsearch domains. Fixes #858 Requires mattclay/aws-terminator#187 ISSUE TYPE New Module Pull Request COMPONENT NAME Creates OpenSearch or ElasticSearch domain. ADDITIONAL INFORMATION The minimum version of botocore for these modules is 1.21.38. The integration tests take more than 4 hours to execute. Tests time out in the CI. I was able to run the integration tests locally. Reviewed-by: Alina Buzachis Reviewed-by: Sebastien Rosset Reviewed-by: Mark Chappell Reviewed-by: Markus Bergholz --- opensearch.py | 1507 ++++++++++++++++++++++++++++++++++++++++++++ opensearch_info.py | 530 ++++++++++++++++ 2 files changed, 2037 insertions(+) create mode 100644 opensearch.py create mode 100644 opensearch_info.py diff --git a/opensearch.py b/opensearch.py new file mode 100644 index 00000000000..422feb7d31a --- /dev/null +++ b/opensearch.py @@ -0,0 +1,1507 @@ +#!/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) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: opensearch +short_description: Creates OpenSearch or ElasticSearch domain. +description: + - Creates or modify a Amazon OpenSearch Service domain. +version_added: 3.1.0 +author: "Sebastien Rosset (@sebastien-rosset)" +options: + state: + description: + - Creates or modifies an existing OpenSearch domain. + - Deletes an OpenSearch domain. + required: false + type: str + choices: ['present', 'absent'] + default: present + domain_name: + description: + - The name of the Amazon OpenSearch/ElasticSearch Service domain. + - Domain names are unique across the domains owned by an account within an AWS region. + required: true + type: str + engine_version: + description: + -> + The engine version to use. For example, 'ElasticSearch_7.10' or 'OpenSearch_1.1'. + -> + If the currently running version is not equal to I(engine_version), + a cluster upgrade is triggered. + -> + It may not be possible to upgrade directly from the currently running version + to I(engine_version). In that case, the upgrade is performed incrementally by + upgrading to the highest compatible version, then repeat the operation until + the cluster is running at the target version. + -> + The upgrade operation fails if there is no path from current version to I(engine_version). + -> + See OpenSearch documentation for upgrade compatibility. + required: false + type: str + allow_intermediate_upgrades: + description: + - > + If true, allow OpenSearch domain to be upgraded through one or more intermediate versions. + - > + If false, do not allow OpenSearch domain to be upgraded through intermediate versions. + The upgrade operation fails if it's not possible to ugrade to I(engine_version) directly. + required: false + type: bool + default: true + cluster_config: + description: + - Parameters for the cluster configuration of an OpenSearch Service domain. + type: dict + suboptions: + instance_type: + description: + - Type of the instances to use for the domain. + required: false + type: str + instance_count: + description: + - Number of instances for the domain. + required: false + type: int + zone_awareness: + description: + - A boolean value to indicate whether zone awareness is enabled. + required: false + type: bool + availability_zone_count: + description: + - > + An integer value to indicate the number of availability zones for a domain when zone awareness is enabled. + This should be equal to number of subnets if VPC endpoints is enabled. + required: false + type: int + dedicated_master: + description: + - A boolean value to indicate whether a dedicated master node is enabled. + required: false + type: bool + dedicated_master_instance_type: + description: + - The instance type for a dedicated master node. + required: false + type: str + dedicated_master_instance_count: + description: + - Total number of dedicated master nodes, active and on standby, for the domain. + required: false + type: int + warm_enabled: + description: + - True to enable UltraWarm storage. + required: false + type: bool + warm_type: + description: + - The instance type for the OpenSearch domain's warm nodes. + required: false + type: str + warm_count: + description: + - The number of UltraWarm nodes in the domain. + required: false + type: int + cold_storage_options: + description: + - Specifies the ColdStorageOptions config for a Domain. + type: dict + suboptions: + enabled: + description: + - True to enable cold storage. Supported on Elasticsearch 7.9 or above. + required: false + type: bool + ebs_options: + description: + - Parameters to configure EBS-based storage for an OpenSearch Service domain. + type: dict + suboptions: + ebs_enabled: + description: + - Specifies whether EBS-based storage is enabled. + required: false + type: bool + volume_type: + description: + - Specifies the volume type for EBS-based storage. "standard"|"gp2"|"io1" + required: false + type: str + volume_size: + description: + - Integer to specify the size of an EBS volume. + required: false + type: int + iops: + description: + - The IOPD for a Provisioned IOPS EBS volume (SSD). + required: false + type: int + vpc_options: + description: + - Options to specify the subnets and security groups for a VPC endpoint. + type: dict + suboptions: + subnets: + description: + - Specifies the subnet ids for VPC endpoint. + required: false + type: list + elements: str + security_groups: + description: + - Specifies the security group ids for VPC endpoint. + required: false + type: list + elements: str + snapshot_options: + description: + - Option to set time, in UTC format, of the daily automated snapshot. + type: dict + suboptions: + automated_snapshot_start_hour: + description: + - > + Integer value from 0 to 23 specifying when the service takes a daily automated snapshot + of the specified Elasticsearch domain. + required: false + type: int + access_policies: + description: + - IAM access policy as a JSON-formatted string. + required: false + type: dict + encryption_at_rest_options: + description: + - Parameters to enable encryption at rest. + type: dict + suboptions: + enabled: + description: + - Should data be encrypted while at rest. + required: false + type: bool + kms_key_id: + description: + - If encryption at rest enabled, this identifies the encryption key to use. + - The value should be a KMS key ARN. It can also be the KMS key id. + required: false + type: str + node_to_node_encryption_options: + description: + - Node-to-node encryption options. + type: dict + suboptions: + enabled: + description: + - True to enable node-to-node encryption. + required: false + type: bool + cognito_options: + description: + - Parameters to configure OpenSearch Service to use Amazon Cognito authentication for OpenSearch Dashboards. + type: dict + suboptions: + enabled: + description: + - The option to enable Cognito for OpenSearch Dashboards authentication. + required: false + type: bool + user_pool_id: + description: + - The Cognito user pool ID for OpenSearch Dashboards authentication. + required: false + type: str + identity_pool_id: + description: + - The Cognito identity pool ID for OpenSearch Dashboards authentication. + required: false + type: str + role_arn: + description: + - The role ARN that provides OpenSearch permissions for accessing Cognito resources. + required: false + type: str + domain_endpoint_options: + description: + - Options to specify configuration that will be applied to the domain endpoint. + type: dict + suboptions: + enforce_https: + description: + - Whether only HTTPS endpoint should be enabled for the domain. + type: bool + tls_security_policy: + description: + - Specify the TLS security policy to apply to the HTTPS endpoint of the domain. + type: str + custom_endpoint_enabled: + description: + - Whether to enable a custom endpoint for the domain. + type: bool + custom_endpoint: + description: + - The fully qualified domain for your custom endpoint. + type: str + custom_endpoint_certificate_arn: + description: + - The ACM certificate ARN for your custom endpoint. + type: str + advanced_security_options: + description: + - Specifies advanced security options. + type: dict + suboptions: + enabled: + description: + - True if advanced security is enabled. + - You must enable node-to-node encryption to use advanced security options. + type: bool + internal_user_database_enabled: + description: + - True if the internal user database is enabled. + type: bool + master_user_options: + description: + - Credentials for the master user, username and password, ARN, or both. + type: dict + suboptions: + master_user_arn: + description: + - ARN for the master user (if IAM is enabled). + type: str + master_user_name: + description: + - The username of the master user, which is stored in the Amazon OpenSearch Service domain internal database. + type: str + master_user_password: + description: + - The password of the master user, which is stored in the Amazon OpenSearch Service domain internal database. + type: str + saml_options: + description: + - The SAML application configuration for the domain. + type: dict + suboptions: + enabled: + description: + - True if SAML is enabled. + - To use SAML authentication, you must enable fine-grained access control. + - You can only enable SAML authentication for OpenSearch Dashboards on existing domains, + not during the creation of new ones. + - Domains only support one Dashboards authentication method at a time. + If you have Amazon Cognito authentication for OpenSearch Dashboards enabled, + you must disable it before you can enable SAML. + type: bool + idp: + description: + - The SAML Identity Provider's information. + type: dict + suboptions: + metadata_content: + description: + - The metadata of the SAML application in XML format. + type: str + entity_id: + description: + - The unique entity ID of the application in SAML identity provider. + type: str + master_user_name: + description: + - The SAML master username, which is stored in the Amazon OpenSearch Service domain internal database. + type: str + master_backend_role: + description: + - The backend role that the SAML master user is mapped to. + type: str + subject_key: + description: + - Element of the SAML assertion to use for username. Default is NameID. + type: str + roles_key: + description: + - Element of the SAML assertion to use for backend roles. Default is roles. + type: str + session_timeout_minutes: + description: + - The duration, in minutes, after which a user session becomes inactive. Acceptable values are between 1 and 1440, and the default value is 60. + type: int + auto_tune_options: + description: + - Specifies Auto-Tune options. + type: dict + suboptions: + desired_state: + description: + - The Auto-Tune desired state. Valid values are ENABLED and DISABLED. + type: str + choices: ['ENABLED', 'DISABLED'] + maintenance_schedules: + description: + - A list of maintenance schedules. + type: list + elements: dict + suboptions: + start_at: + description: + - The timestamp at which the Auto-Tune maintenance schedule starts. + type: str + duration: + description: + - Specifies maintenance schedule duration, duration value and duration unit. + type: dict + suboptions: + value: + description: + - Integer to specify the value of a maintenance schedule duration. + type: int + unit: + description: + - The unit of a maintenance schedule duration. Valid value is HOURS. + choices: ['HOURS'] + type: str + cron_expression_for_recurrence: + description: + - A cron expression for a recurring maintenance schedule. + type: str + wait: + description: + - Whether or not to wait for completion of OpenSearch creation, modification or deletion. + type: bool + default: 'no' + wait_timeout: + description: + - how long before wait gives up, in seconds. + default: 300 + type: int + tags: + description: + - tags dict to apply to an OpenSearch cluster. + type: dict + purge_tags: + description: + - whether to remove tags not present in the C(tags) parameter. + default: True + type: bool +requirements: +- botocore >= 1.21.38 +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 +""" + +EXAMPLES = """ + +- name: Create OpenSearch domain for dev environment, no zone awareness, no dedicated masters + community.aws.opensearch: + domain_name: "dev-cluster" + engine_version: Elasticsearch_1.1 + cluster_config: + instance_type: "t2.small.search" + instance_count: 2 + zone_awareness: false + dedicated_master: false + ebs_options: + ebs_enabled: true + volume_type: "gp2" + volume_size: 10 + access_policies: "{{ lookup('file', 'policy.json') | from_json }}" + +- name: Create OpenSearch domain with dedicated masters + community.aws.opensearch: + domain_name: "my-domain" + engine_version: OpenSearch_1.1 + cluster_config: + instance_type: "t2.small.search" + instance_count: 12 + dedicated_master: true + zone_awareness: true + availability_zone_count: 2 + dedicated_master_instance_type: "t2.small.search" + dedicated_master_instance_count: 3 + warm_enabled: true + warm_type: "ultrawarm1.medium.search" + warm_count: 1 + cold_storage_options: + enabled: false + ebs_options: + ebs_enabled: true + volume_type: "io1" + volume_size: 10 + iops: 1000 + vpc_options: + subnets: + - "subnet-e537d64a" + - "subnet-e537d64b" + security_groups: + - "sg-dd2f13cb" + - "sg-dd2f13cc" + snapshot_options: + automated_snapshot_start_hour: 13 + access_policies: "{{ lookup('file', 'policy.json') | from_json }}" + encryption_at_rest_options: + enabled: false + node_to_node_encryption_options: + enabled: false + auto_tune_options: + enabled: true + maintenance_schedules: + - start_at: "2025-01-12" + duration: + value: 1 + unit: "HOURS" + cron_expression_for_recurrence: "cron(0 12 * * ? *)" + - start_at: "2032-01-12" + duration: + value: 2 + unit: "HOURS" + cron_expression_for_recurrence: "cron(0 12 * * ? *)" + tags: + Environment: Development + Application: Search + wait: true + +- name: Increase size of EBS volumes for existing cluster + community.aws.opensearch: + domain_name: "my-domain" + ebs_options: + volume_size: 5 + wait: true + +- name: Increase instance count for existing cluster + community.aws.opensearch: + domain_name: "my-domain" + cluster_config: + instance_count: 40 + wait: true + +""" + +from copy import deepcopy +import datetime +import json + +try: + import botocore +except ImportError: + pass # handled by AnsibleAWSModule + +from ansible.module_utils.six import string_types + +# import module snippets +from ansible_collections.amazon.aws.plugins.module_utils.core import ( + AnsibleAWSModule, + is_boto3_error_code, +) +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ( + AWSRetry, + boto3_tag_list_to_ansible_dict, + compare_policies, +) +from ansible_collections.community.aws.plugins.module_utils.opensearch import ( + compare_domain_versions, + ensure_tags, + get_domain_status, + get_domain_config, + get_target_increment_version, + normalize_opensearch, + parse_version, + wait_for_domain_status, +) + + +def ensure_domain_absent(client, module): + domain_name = module.params.get("domain_name") + changed = False + + domain = get_domain_status(client, module, domain_name) + if module.check_mode: + module.exit_json( + changed=True, msg="Would have deleted domain if not in check mode" + ) + try: + client.delete_domain(DomainName=domain_name) + changed = True + except is_boto3_error_code("ResourceNotFoundException"): + # The resource does not exist, or it has already been deleted + return dict(changed=False) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="trying to delete domain") + + # If we're not waiting for a delete to complete then we're all done + # so just return + if not domain or not module.params.get("wait"): + return dict(changed=changed) + try: + wait_for_domain_status(client, module, domain_name, "domain_deleted") + return dict(changed=changed) + except is_boto3_error_code("ResourceNotFoundException"): + return dict(changed=changed) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, "awaiting domain deletion") + + +def upgrade_domain(client, module, source_version, target_engine_version): + domain_name = module.params.get("domain_name") + # Determine if it's possible to upgrade directly from source version + # to target version, or if it's necessary to upgrade through intermediate major versions. + next_version = target_engine_version + # When perform_check_only is true, indicates that an upgrade eligibility check needs + # to be performed. Does not actually perform the upgrade. + perform_check_only = False + if module.check_mode: + perform_check_only = True + current_version = source_version + while current_version != target_engine_version: + v = get_target_increment_version(client, module, domain_name, target_engine_version) + if v is None: + # There is no compatible version, according to the get_compatible_versions() API. + # The upgrade should fail, but try anyway. + next_version = target_engine_version + if next_version != target_engine_version: + # It's not possible to upgrade directly to the target version. + # Check the module parameters to determine if this is allowed or not. + if not module.params.get("allow_intermediate_upgrades"): + module.fail_json(msg="Cannot upgrade from {0} to version {1}. The highest compatible version is {2}".format( + source_version, target_engine_version, next_version)) + + parameters = { + "DomainName": domain_name, + "TargetVersion": next_version, + "PerformCheckOnly": perform_check_only, + } + + if not module.check_mode: + # If background tasks are in progress, wait until they complete. + # This can take several hours depending on the cluster size and the type of background tasks + # (maybe an upgrade is already in progress). + # It's not possible to upgrade a domain that has background tasks are in progress, + # the call to client.upgrade_domain would fail. + wait_for_domain_status(client, module, domain_name, "domain_available") + + try: + client.upgrade_domain(**parameters) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + # In check mode (=> PerformCheckOnly==True), a ValidationException may be + # raised if it's not possible to upgrade to the target version. + module.fail_json_aws( + e, + msg="Couldn't upgrade domain {0} from {1} to {2}".format( + domain_name, current_version, next_version + ), + ) + + if module.check_mode: + module.exit_json( + changed=True, + msg="Would have upgraded domain from {0} to {1} if not in check mode".format( + current_version, next_version + ), + ) + current_version = next_version + + if module.params.get("wait"): + wait_for_domain_status(client, module, domain_name, "domain_available") + + +def set_cluster_config( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + + cluster_config = desired_domain_config["ClusterConfig"] + cluster_opts = module.params.get("cluster_config") + if cluster_opts is not None: + if cluster_opts.get("instance_type") is not None: + cluster_config["InstanceType"] = cluster_opts.get("instance_type") + if cluster_opts.get("instance_count") is not None: + cluster_config["InstanceCount"] = cluster_opts.get("instance_count") + if cluster_opts.get("zone_awareness") is not None: + cluster_config["ZoneAwarenessEnabled"] = cluster_opts.get("zone_awareness") + if cluster_config["ZoneAwarenessEnabled"]: + if cluster_opts.get("availability_zone_count") is not None: + cluster_config["ZoneAwarenessConfig"] = { + "AvailabilityZoneCount": cluster_opts.get( + "availability_zone_count" + ), + } + + if cluster_opts.get("dedicated_master") is not None: + cluster_config["DedicatedMasterEnabled"] = cluster_opts.get( + "dedicated_master" + ) + if cluster_config["DedicatedMasterEnabled"]: + if cluster_opts.get("dedicated_master_instance_type") is not None: + cluster_config["DedicatedMasterType"] = cluster_opts.get( + "dedicated_master_instance_type" + ) + if cluster_opts.get("dedicated_master_instance_count") is not None: + cluster_config["DedicatedMasterCount"] = cluster_opts.get( + "dedicated_master_instance_count" + ) + + if cluster_opts.get("warm_enabled") is not None: + cluster_config["WarmEnabled"] = cluster_opts.get("warm_enabled") + if cluster_config["WarmEnabled"]: + if cluster_opts.get("warm_type") is not None: + cluster_config["WarmType"] = cluster_opts.get("warm_type") + if cluster_opts.get("warm_count") is not None: + cluster_config["WarmCount"] = cluster_opts.get("warm_count") + + cold_storage_opts = None + if cluster_opts is not None: + cold_storage_opts = cluster_opts.get("cold_storage_options") + if compare_domain_versions(desired_domain_config["EngineVersion"], "Elasticsearch_7.9") < 0: + # If the engine version is ElasticSearch < 7.9, cold storage is not supported. + # When querying a domain < 7.9, the AWS API indicates cold storage is disabled (Enabled: False), + # which makes sense. However, trying to do HTTP POST with Enable: False causes an API error. + # The 'ColdStorageOptions' attribute should not be present in HTTP POST. + if cold_storage_opts is not None and cold_storage_opts.get("enabled"): + module.fail_json(msg="Cold Storage is not supported") + cluster_config.pop("ColdStorageOptions", None) + if ( + current_domain_config is not None + and "ClusterConfig" in current_domain_config + ): + # Remove 'ColdStorageOptions' from the current domain config, otherwise the actual vs desired diff + # will indicate a change must be done. + current_domain_config["ClusterConfig"].pop("ColdStorageOptions", None) + else: + # Elasticsearch 7.9 and above support ColdStorageOptions. + if ( + cold_storage_opts is not None + and cold_storage_opts.get("enabled") is not None + ): + cluster_config["ColdStorageOptions"] = { + "Enabled": cold_storage_opts.get("enabled"), + } + + if ( + current_domain_config is not None + and current_domain_config["ClusterConfig"] != cluster_config + ): + change_set.append( + "ClusterConfig changed from {0} to {1}".format( + current_domain_config["ClusterConfig"], cluster_config + ) + ) + changed = True + return changed + + +def set_ebs_options(module, current_domain_config, desired_domain_config, change_set): + changed = False + ebs_config = desired_domain_config["EBSOptions"] + ebs_opts = module.params.get("ebs_options") + if ebs_opts is None: + return changed + if ebs_opts.get("ebs_enabled") is not None: + ebs_config["EBSEnabled"] = ebs_opts.get("ebs_enabled") + + if not ebs_config["EBSEnabled"]: + desired_domain_config["EBSOptions"] = { + "EBSEnabled": False, + } + else: + if ebs_opts.get("volume_type") is not None: + ebs_config["VolumeType"] = ebs_opts.get("volume_type") + if ebs_opts.get("volume_size") is not None: + ebs_config["VolumeSize"] = ebs_opts.get("volume_size") + if ebs_opts.get("iops") is not None: + ebs_config["Iops"] = ebs_opts.get("iops") + + if ( + current_domain_config is not None + and current_domain_config["EBSOptions"] != ebs_config + ): + change_set.append( + "EBSOptions changed from {0} to {1}".format( + current_domain_config["EBSOptions"], ebs_config + ) + ) + changed = True + return changed + + +def set_encryption_at_rest_options( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + encryption_at_rest_config = desired_domain_config["EncryptionAtRestOptions"] + encryption_at_rest_opts = module.params.get("encryption_at_rest_options") + if encryption_at_rest_opts is None: + return False + if encryption_at_rest_opts.get("enabled") is not None: + encryption_at_rest_config["Enabled"] = encryption_at_rest_opts.get("enabled") + if not encryption_at_rest_config["Enabled"]: + desired_domain_config["EncryptionAtRestOptions"] = { + "Enabled": False, + } + else: + if encryption_at_rest_opts.get("kms_key_id") is not None: + encryption_at_rest_config["KmsKeyId"] = encryption_at_rest_opts.get( + "kms_key_id" + ) + + if ( + current_domain_config is not None + and current_domain_config["EncryptionAtRestOptions"] + != encryption_at_rest_config + ): + change_set.append( + "EncryptionAtRestOptions changed from {0} to {1}".format( + current_domain_config["EncryptionAtRestOptions"], + encryption_at_rest_config, + ) + ) + changed = True + return changed + + +def set_node_to_node_encryption_options( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + node_to_node_encryption_config = desired_domain_config[ + "NodeToNodeEncryptionOptions" + ] + node_to_node_encryption_opts = module.params.get("node_to_node_encryption_options") + if node_to_node_encryption_opts is None: + return changed + if node_to_node_encryption_opts.get("enabled") is not None: + node_to_node_encryption_config["Enabled"] = node_to_node_encryption_opts.get( + "enabled" + ) + + if ( + current_domain_config is not None + and current_domain_config["NodeToNodeEncryptionOptions"] + != node_to_node_encryption_config + ): + change_set.append( + "NodeToNodeEncryptionOptions changed from {0} to {1}".format( + current_domain_config["NodeToNodeEncryptionOptions"], + node_to_node_encryption_config, + ) + ) + changed = True + return changed + + +def set_vpc_options(module, current_domain_config, desired_domain_config, change_set): + changed = False + vpc_config = None + if "VPCOptions" in desired_domain_config: + vpc_config = desired_domain_config["VPCOptions"] + vpc_opts = module.params.get("vpc_options") + if vpc_opts is None: + return changed + vpc_subnets = vpc_opts.get("subnets") + if vpc_subnets is not None: + if vpc_config is None: + vpc_config = {} + desired_domain_config["VPCOptions"] = vpc_config + # OpenSearch cluster is attached to VPC + if isinstance(vpc_subnets, string_types): + vpc_subnets = [x.strip() for x in vpc_subnets.split(",")] + vpc_config["SubnetIds"] = vpc_subnets + + vpc_security_groups = vpc_opts.get("security_groups") + if vpc_security_groups is not None: + if vpc_config is None: + vpc_config = {} + desired_domain_config["VPCOptions"] = vpc_config + if isinstance(vpc_security_groups, string_types): + vpc_security_groups = [x.strip() for x in vpc_security_groups.split(",")] + vpc_config["SecurityGroupIds"] = vpc_security_groups + + if current_domain_config is not None: + # Modify existing cluster. + current_cluster_is_vpc = False + desired_cluster_is_vpc = False + if ( + "VPCOptions" in current_domain_config + and "SubnetIds" in current_domain_config["VPCOptions"] + and len(current_domain_config["VPCOptions"]["SubnetIds"]) > 0 + ): + current_cluster_is_vpc = True + if ( + "VPCOptions" in desired_domain_config + and "SubnetIds" in desired_domain_config["VPCOptions"] + and len(desired_domain_config["VPCOptions"]["SubnetIds"]) > 0 + ): + desired_cluster_is_vpc = True + if current_cluster_is_vpc != desired_cluster_is_vpc: + # AWS does not allow changing the type. Don't fail here so we return the AWS API error. + change_set.append("VPCOptions changed between Internet and VPC") + changed = True + elif desired_cluster_is_vpc is False: + # There are no VPCOptions to configure. + pass + else: + # Note the subnets may be the same but be listed in a different order. + if set(current_domain_config["VPCOptions"]["SubnetIds"]) != set( + vpc_config["SubnetIds"] + ): + change_set.append( + "SubnetIds changed from {0} to {1}".format( + current_domain_config["VPCOptions"]["SubnetIds"], + vpc_config["SubnetIds"], + ) + ) + changed = True + if set(current_domain_config["VPCOptions"]["SecurityGroupIds"]) != set( + vpc_config["SecurityGroupIds"] + ): + change_set.append( + "SecurityGroup changed from {0} to {1}".format( + current_domain_config["VPCOptions"]["SecurityGroupIds"], + vpc_config["SecurityGroupIds"], + ) + ) + changed = True + return changed + + +def set_snapshot_options( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + snapshot_config = desired_domain_config["SnapshotOptions"] + snapshot_opts = module.params.get("snapshot_options") + if snapshot_opts is None: + return changed + if snapshot_opts.get("automated_snapshot_start_hour") is not None: + snapshot_config["AutomatedSnapshotStartHour"] = snapshot_opts.get( + "automated_snapshot_start_hour" + ) + if ( + current_domain_config is not None + and current_domain_config["SnapshotOptions"] != snapshot_config + ): + change_set.append("SnapshotOptions changed") + changed = True + return changed + + +def set_cognito_options( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + cognito_config = desired_domain_config["CognitoOptions"] + cognito_opts = module.params.get("cognito_options") + if cognito_opts is None: + return changed + if cognito_opts.get("enabled") is not None: + cognito_config["Enabled"] = cognito_opts.get("enabled") + if not cognito_config["Enabled"]: + desired_domain_config["CognitoOptions"] = { + "Enabled": False, + } + else: + if cognito_opts.get("cognito_user_pool_id") is not None: + cognito_config["UserPoolId"] = cognito_opts.get("cognito_user_pool_id") + if cognito_opts.get("cognito_identity_pool_id") is not None: + cognito_config["IdentityPoolId"] = cognito_opts.get( + "cognito_identity_pool_id" + ) + if cognito_opts.get("cognito_role_arn") is not None: + cognito_config["RoleArn"] = cognito_opts.get("cognito_role_arn") + + if ( + current_domain_config is not None + and current_domain_config["CognitoOptions"] != cognito_config + ): + change_set.append( + "CognitoOptions changed from {0} to {1}".format( + current_domain_config["CognitoOptions"], cognito_config + ) + ) + changed = True + return changed + + +def set_advanced_security_options( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + advanced_security_config = desired_domain_config["AdvancedSecurityOptions"] + advanced_security_opts = module.params.get("advanced_security_options") + if advanced_security_opts is None: + return changed + if advanced_security_opts.get("enabled") is not None: + advanced_security_config["Enabled"] = advanced_security_opts.get("enabled") + if not advanced_security_config["Enabled"]: + desired_domain_config["AdvancedSecurityOptions"] = { + "Enabled": False, + } + else: + if advanced_security_opts.get("internal_user_database_enabled") is not None: + advanced_security_config[ + "InternalUserDatabaseEnabled" + ] = advanced_security_opts.get("internal_user_database_enabled") + master_user_opts = advanced_security_opts.get("master_user_options") + if master_user_opts is not None: + if master_user_opts.get("master_user_arn") is not None: + advanced_security_config["MasterUserOptions"][ + "MasterUserARN" + ] = master_user_opts.get("master_user_arn") + if master_user_opts.get("master_user_name") is not None: + advanced_security_config["MasterUserOptions"][ + "MasterUserName" + ] = master_user_opts.get("master_user_name") + if master_user_opts.get("master_user_password") is not None: + advanced_security_config["MasterUserOptions"][ + "MasterUserPassword" + ] = master_user_opts.get("master_user_password") + saml_opts = advanced_security_opts.get("saml_options") + if saml_opts is not None: + if saml_opts.get("enabled") is not None: + advanced_security_config["SamlOptions"]["Enabled"] = saml_opts.get( + "enabled" + ) + idp_opts = saml_opts.get("idp") + if idp_opts is not None: + if idp_opts.get("metadata_content") is not None: + advanced_security_config["SamlOptions"]["Idp"][ + "MetadataContent" + ] = idp_opts.get("metadata_content") + if idp_opts.get("entity_id") is not None: + advanced_security_config["SamlOptions"]["Idp"][ + "EntityId" + ] = idp_opts.get("entity_id") + if saml_opts.get("master_user_name") is not None: + advanced_security_config["SamlOptions"][ + "MasterUserName" + ] = saml_opts.get("master_user_name") + if saml_opts.get("master_backend_role") is not None: + advanced_security_config["SamlOptions"][ + "MasterBackendRole" + ] = saml_opts.get("master_backend_role") + if saml_opts.get("subject_key") is not None: + advanced_security_config["SamlOptions"]["SubjectKey"] = saml_opts.get( + "subject_key" + ) + if saml_opts.get("roles_key") is not None: + advanced_security_config["SamlOptions"]["RolesKey"] = saml_opts.get( + "roles_key" + ) + if saml_opts.get("session_timeout_minutes") is not None: + advanced_security_config["SamlOptions"][ + "SessionTimeoutMinutes" + ] = saml_opts.get("session_timeout_minutes") + + if ( + current_domain_config is not None + and current_domain_config["AdvancedSecurityOptions"] != advanced_security_config + ): + change_set.append( + "AdvancedSecurityOptions changed from {0} to {1}".format( + current_domain_config["AdvancedSecurityOptions"], + advanced_security_config, + ) + ) + changed = True + return changed + + +def set_domain_endpoint_options( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + domain_endpoint_config = desired_domain_config["DomainEndpointOptions"] + domain_endpoint_opts = module.params.get("domain_endpoint_options") + if domain_endpoint_opts is None: + return changed + if domain_endpoint_opts.get("enforce_https") is not None: + domain_endpoint_config["EnforceHTTPS"] = domain_endpoint_opts.get( + "enforce_https" + ) + if domain_endpoint_opts.get("tls_security_policy") is not None: + domain_endpoint_config["TLSSecurityPolicy"] = domain_endpoint_opts.get( + "tls_security_policy" + ) + if domain_endpoint_opts.get("custom_endpoint_enabled") is not None: + domain_endpoint_config["CustomEndpointEnabled"] = domain_endpoint_opts.get( + "custom_endpoint_enabled" + ) + if domain_endpoint_config["CustomEndpointEnabled"]: + if domain_endpoint_opts.get("custom_endpoint") is not None: + domain_endpoint_config["CustomEndpoint"] = domain_endpoint_opts.get( + "custom_endpoint" + ) + if domain_endpoint_opts.get("custom_endpoint_certificate_arn") is not None: + domain_endpoint_config[ + "CustomEndpointCertificateArn" + ] = domain_endpoint_opts.get("custom_endpoint_certificate_arn") + + if ( + current_domain_config is not None + and current_domain_config["DomainEndpointOptions"] != domain_endpoint_config + ): + change_set.append( + "DomainEndpointOptions changed from {0} to {1}".format( + current_domain_config["DomainEndpointOptions"], domain_endpoint_config + ) + ) + changed = True + return changed + + +def set_auto_tune_options( + module, current_domain_config, desired_domain_config, change_set +): + changed = False + auto_tune_config = desired_domain_config["AutoTuneOptions"] + auto_tune_opts = module.params.get("auto_tune_options") + if auto_tune_opts is None: + return changed + schedules = auto_tune_opts.get("maintenance_schedules") + if auto_tune_opts.get("desired_state") is not None: + auto_tune_config["DesiredState"] = auto_tune_opts.get("desired_state") + if auto_tune_config["DesiredState"] != "ENABLED": + desired_domain_config["AutoTuneOptions"] = { + "DesiredState": "DISABLED", + } + elif schedules is not None: + auto_tune_config["MaintenanceSchedules"] = [] + for s in schedules: + schedule_entry = {} + start_at = s.get("start_at") + if start_at is not None: + if isinstance(start_at, datetime.datetime): + # The property was parsed from yaml to datetime, but the AWS API wants a string + start_at = start_at.strftime("%Y-%m-%d") + schedule_entry["StartAt"] = start_at + duration_opt = s.get("duration") + if duration_opt is not None: + schedule_entry["Duration"] = {} + if duration_opt.get("value") is not None: + schedule_entry["Duration"]["Value"] = duration_opt.get("value") + if duration_opt.get("unit") is not None: + schedule_entry["Duration"]["Unit"] = duration_opt.get("unit") + if s.get("cron_expression_for_recurrence") is not None: + schedule_entry["CronExpressionForRecurrence"] = s.get( + "cron_expression_for_recurrence" + ) + auto_tune_config["MaintenanceSchedules"].append(schedule_entry) + if current_domain_config is not None: + if ( + current_domain_config["AutoTuneOptions"]["DesiredState"] + != auto_tune_config["DesiredState"] + ): + change_set.append( + "AutoTuneOptions.DesiredState changed from {0} to {1}".format( + current_domain_config["AutoTuneOptions"]["DesiredState"], + auto_tune_config["DesiredState"], + ) + ) + changed = True + if ( + auto_tune_config["MaintenanceSchedules"] + != current_domain_config["AutoTuneOptions"]["MaintenanceSchedules"] + ): + change_set.append( + "AutoTuneOptions.MaintenanceSchedules changed from {0} to {1}".format( + current_domain_config["AutoTuneOptions"]["MaintenanceSchedules"], + auto_tune_config["MaintenanceSchedules"], + ) + ) + changed = True + return changed + + +def set_access_policy(module, current_domain_config, desired_domain_config, change_set): + access_policy_config = None + changed = False + access_policy_opt = module.params.get("access_policies") + if access_policy_opt is None: + return changed + try: + access_policy_config = json.dumps(access_policy_opt) + except Exception as e: + module.fail_json( + msg="Failed to convert the policy into valid JSON: %s" % str(e) + ) + if current_domain_config is not None: + # Updating existing domain + current_access_policy = json.loads(current_domain_config["AccessPolicies"]) + if not compare_policies(current_access_policy, access_policy_opt): + change_set.append( + "AccessPolicy changed from {0} to {1}".format( + current_access_policy, access_policy_opt + ) + ) + changed = True + desired_domain_config["AccessPolicies"] = access_policy_config + else: + # Creating new domain + desired_domain_config["AccessPolicies"] = access_policy_config + return changed + + +def ensure_domain_present(client, module): + domain_name = module.params.get("domain_name") + + # Create default if OpenSearch does not exist. If domain already exists, + # the data is populated by retrieving the current configuration from the API. + desired_domain_config = { + "DomainName": module.params.get("domain_name"), + "EngineVersion": "OpenSearch_1.1", + "ClusterConfig": { + "InstanceType": "t2.small.search", + "InstanceCount": 2, + "ZoneAwarenessEnabled": False, + "DedicatedMasterEnabled": False, + "WarmEnabled": False, + }, + # By default create ES attached to the Internet. + # If the "VPCOptions" property is specified, even if empty, the API server interprets + # as incomplete VPC configuration. + # "VPCOptions": {}, + "EBSOptions": { + "EBSEnabled": False, + }, + "EncryptionAtRestOptions": { + "Enabled": False, + }, + "NodeToNodeEncryptionOptions": { + "Enabled": False, + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 0, + }, + "CognitoOptions": { + "Enabled": False, + }, + "AdvancedSecurityOptions": { + "Enabled": False, + }, + "DomainEndpointOptions": { + "CustomEndpointEnabled": False, + }, + "AutoTuneOptions": { + "DesiredState": "DISABLED", + }, + } + # Determine if OpenSearch domain already exists. + # current_domain_config may be None if the domain does not exist. + (current_domain_config, domain_arn) = get_domain_config(client, module, domain_name) + if current_domain_config is not None: + desired_domain_config = deepcopy(current_domain_config) + + if module.params.get("engine_version") is not None: + # Validate the engine_version + v = parse_version(module.params.get("engine_version")) + if v is None: + module.fail_json( + "Invalid engine_version. Must be Elasticsearch_X.Y or OpenSearch_X.Y" + ) + desired_domain_config["EngineVersion"] = module.params.get("engine_version") + + changed = False + change_set = [] # For check mode purpose + + changed |= set_cluster_config( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_ebs_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_encryption_at_rest_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_node_to_node_encryption_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_vpc_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_snapshot_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_cognito_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_advanced_security_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_domain_endpoint_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_auto_tune_options( + module, current_domain_config, desired_domain_config, change_set + ) + changed |= set_access_policy( + module, current_domain_config, desired_domain_config, change_set + ) + + if current_domain_config is not None: + if ( + desired_domain_config["EngineVersion"] + != current_domain_config["EngineVersion"] + ): + changed = True + change_set.append("EngineVersion changed") + upgrade_domain( + client, + module, + current_domain_config["EngineVersion"], + desired_domain_config["EngineVersion"], + ) + + if changed: + if module.check_mode: + module.exit_json( + changed=True, + msg=f"Would have updated domain if not in check mode: {change_set}", + ) + # Remove the "EngineVersion" attribute, the AWS API does not accept this attribute. + desired_domain_config.pop("EngineVersion", None) + try: + client.update_domain_config(**desired_domain_config) + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: + module.fail_json_aws( + e, msg="Couldn't update domain {0}".format(domain_name) + ) + + else: + # Create new OpenSearch cluster + if module.params.get("access_policies") is None: + module.fail_json( + "state is present but the following is missing: access_policies" + ) + + changed = True + if module.check_mode: + module.exit_json( + changed=True, msg="Would have created a domain if not in check mode" + ) + try: + response = client.create_domain(**desired_domain_config) + domain = response["DomainStatus"] + domain_arn = domain["ARN"] + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: + module.fail_json_aws( + e, msg="Couldn't update domain {0}".format(domain_name) + ) + + try: + existing_tags = boto3_tag_list_to_ansible_dict( + client.list_tags(ARN=domain_arn, aws_retry=True)["TagList"] + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, "Couldn't get tags for domain %s" % domain_name) + + desired_tags = module.params["tags"] + purge_tags = module.params["purge_tags"] + changed |= ensure_tags( + client, module, domain_arn, existing_tags, desired_tags, purge_tags + ) + + if module.params.get("wait") and not module.check_mode: + wait_for_domain_status(client, module, domain_name, "domain_available") + + domain = get_domain_status(client, module, domain_name) + + return dict(changed=changed, **normalize_opensearch(client, module, domain)) + + +def main(): + + module = AnsibleAWSModule( + argument_spec=dict( + state=dict(choices=["present", "absent"], default="present"), + domain_name=dict(required=True), + engine_version=dict(), + allow_intermediate_upgrades=dict(required=False, type="bool", default=True), + access_policies=dict(required=False, type="dict"), + cluster_config=dict( + type="dict", + default=None, + options=dict( + instance_type=dict(), + instance_count=dict(required=False, type="int"), + zone_awareness=dict(required=False, type="bool"), + availability_zone_count=dict(required=False, type="int"), + dedicated_master=dict(required=False, type="bool"), + dedicated_master_instance_type=dict(), + dedicated_master_instance_count=dict(type="int"), + warm_enabled=dict(required=False, type="bool"), + warm_type=dict(required=False), + warm_count=dict(required=False, type="int"), + cold_storage_options=dict( + type="dict", + default=None, + options=dict( + enabled=dict(required=False, type="bool"), + ), + ), + ), + ), + snapshot_options=dict( + type="dict", + default=None, + options=dict( + automated_snapshot_start_hour=dict(required=False, type="int"), + ), + ), + ebs_options=dict( + type="dict", + default=None, + options=dict( + ebs_enabled=dict(required=False, type="bool"), + volume_type=dict(required=False), + volume_size=dict(required=False, type="int"), + iops=dict(required=False, type="int"), + ), + ), + vpc_options=dict( + type="dict", + default=None, + options=dict( + subnets=dict(type="list", elements="str", required=False), + security_groups=dict(type="list", elements="str", required=False), + ), + ), + cognito_options=dict( + type="dict", + default=None, + options=dict( + enabled=dict(required=False, type="bool"), + user_pool_id=dict(required=False), + identity_pool_id=dict(required=False), + role_arn=dict(required=False, no_log=False), + ), + ), + encryption_at_rest_options=dict( + type="dict", + default=None, + options=dict( + enabled=dict(type="bool"), + kms_key_id=dict(required=False), + ), + ), + node_to_node_encryption_options=dict( + type="dict", + default=None, + options=dict( + enabled=dict(type="bool"), + ), + ), + domain_endpoint_options=dict( + type="dict", + default=None, + options=dict( + enforce_https=dict(type="bool"), + tls_security_policy=dict(), + custom_endpoint_enabled=dict(type="bool"), + custom_endpoint=dict(), + custom_endpoint_certificate_arn=dict(), + ), + ), + advanced_security_options=dict( + type="dict", + default=None, + options=dict( + enabled=dict(type="bool"), + internal_user_database_enabled=dict(type="bool"), + master_user_options=dict( + type="dict", + default=None, + options=dict( + master_user_arn=dict(), + master_user_name=dict(), + master_user_password=dict(no_log=True), + ), + ), + saml_options=dict( + type="dict", + default=None, + options=dict( + enabled=dict(type="bool"), + idp=dict( + type="dict", + default=None, + options=dict( + metadata_content=dict(), + entity_id=dict(), + ), + ), + master_user_name=dict(), + master_backend_role=dict(), + subject_key=dict(no_log=False), + roles_key=dict(no_log=False), + session_timeout_minutes=dict(type="int"), + ), + ), + ), + ), + auto_tune_options=dict( + type="dict", + default=None, + options=dict( + desired_state=dict(choices=["ENABLED", "DISABLED"]), + maintenance_schedules=dict( + type="list", + elements="dict", + default=None, + options=dict( + start_at=dict(), + duration=dict( + type="dict", + default=None, + options=dict( + value=dict(type="int"), + unit=dict(choices=["HOURS"]), + ), + ), + cron_expression_for_recurrence=dict(), + ), + ), + ), + ), + tags=dict(type="dict"), + purge_tags=dict(type="bool", default=True), + wait=dict(type="bool", default=False), + wait_timeout=dict(type="int", default=300), + ), + supports_check_mode=True, + ) + + module.require_botocore_at_least("1.21.38") + + try: + client = module.client("opensearch", 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 opensearch service") + + if module.params["state"] == "absent": + ret_dict = ensure_domain_absent(client, module) + else: + ret_dict = ensure_domain_present(client, module) + + module.exit_json(**ret_dict) + + +if __name__ == "__main__": + main() diff --git a/opensearch_info.py b/opensearch_info.py new file mode 100644 index 00000000000..6a884fdb076 --- /dev/null +++ b/opensearch_info.py @@ -0,0 +1,530 @@ +#!/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) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: opensearch_info +short_description: obtain information about one or more OpenSearch or ElasticSearch domain. +description: + - obtain information about one Amazon OpenSearch Service domain. +version_added: 3.1.0 +author: "Sebastien Rosset (@sebastien-rosset)" +options: + domain_name: + description: + - The name of the Amazon OpenSearch/ElasticSearch Service domain. + required: false + type: str + tags: + description: + - > + A dict of tags that are used to filter OpenSearch domains that match + all tag key, value pairs. + required: false + type: dict +requirements: +- botocore >= 1.21.38 +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 +""" + +EXAMPLES = ''' +- name: Get information about an OpenSearch domain instance + community.aws.opensearch_info: + domain-name: my-search-cluster + register: new_cluster_info + +- name: Get all OpenSearch instances + community.aws.opensearch_info: + +- name: Get all OpenSearch instances that have the specified Key, Value tags + community.aws.opensearch_info: + tags: + Applications: search + Environment: Development +''' + +RETURN = ''' +instances: + description: List of OpenSearch domain instances + returned: always + type: complex + contains: + domain_status: + description: The current status of the OpenSearch domain. + returned: always + type: complex + contains: + arn: + description: The ARN of the OpenSearch domain. + returned: always + type: str + domain_id: + description: The unique identifier for the OpenSearch domain. + returned: always + type: str + domain_name: + description: The name of the OpenSearch domain. + returned: always + type: str + created: + description: + - > + The domain creation status. True if the creation of a domain is complete. + False if domain creation is still in progress. + returned: always + type: bool + deleted: + description: + - > + The domain deletion status. + True if a delete request has been received for the domain but resource cleanup is still in progress. + False if the domain has not been deleted. + Once domain deletion is complete, the status of the domain is no longer returned. + returned: always + type: bool + endpoint: + description: The domain endpoint that you use to submit index and search requests. + returned: always + type: str + endpoints: + description: + - > + Map containing the domain endpoints used to submit index and search requests. + - > + When you create a domain attached to a VPC domain, this propery contains + the DNS endpoint to which service requests are submitted. + - > + If you query the opensearch_info immediately after creating the OpenSearch cluster, + the VPC endpoint may not be returned. It may take several minutes until the + endpoints is available. + type: dict + processing: + description: + - > + The status of the domain configuration. + True if Amazon OpenSearch Service is processing configuration changes. + False if the configuration is active. + returned: always + type: bool + upgrade_processing: + description: true if a domain upgrade operation is in progress. + returned: always + type: bool + engine_version: + description: The version of the OpenSearch domain. + returned: always + type: str + sample: OpenSearch_1.1 + cluster_config: + description: + - Parameters for the cluster configuration of an OpenSearch Service domain. + type: complex + contains: + instance_type: + description: + - Type of the instances to use for the domain. + type: str + instance_count: + description: + - Number of instances for the domain. + type: int + zone_awareness: + description: + - A boolean value to indicate whether zone awareness is enabled. + type: bool + availability_zone_count: + description: + - > + An integer value to indicate the number of availability zones for a domain when zone awareness is enabled. + This should be equal to number of subnets if VPC endpoints is enabled. + type: int + dedicated_master_enabled: + description: + - A boolean value to indicate whether a dedicated master node is enabled. + type: bool + zone_awareness_enabled: + description: + - A boolean value to indicate whether zone awareness is enabled. + type: bool + zone_awareness_config: + description: + - The zone awareness configuration for a domain when zone awareness is enabled. + type: complex + contains: + availability_zone_count: + description: + - An integer value to indicate the number of availability zones for a domain when zone awareness is enabled. + type: int + dedicated_master_type: + description: + - The instance type for a dedicated master node. + type: str + dedicated_master_count: + description: + - Total number of dedicated master nodes, active and on standby, for the domain. + type: int + warm_enabled: + description: + - True to enable UltraWarm storage. + type: bool + warm_type: + description: + - The instance type for the OpenSearch domain's warm nodes. + type: str + warm_count: + description: + - The number of UltraWarm nodes in the domain. + type: int + cold_storage_options: + description: + - Specifies the ColdStorageOptions config for a Domain. + type: complex + contains: + enabled: + description: + - True to enable cold storage. Supported on Elasticsearch 7.9 or above. + type: bool + ebs_options: + description: + - Parameters to configure EBS-based storage for an OpenSearch Service domain. + type: complex + contains: + ebs_enabled: + description: + - Specifies whether EBS-based storage is enabled. + type: bool + volume_type: + description: + - Specifies the volume type for EBS-based storage. "standard"|"gp2"|"io1" + type: str + volume_size: + description: + - Integer to specify the size of an EBS volume. + type: int + iops: + description: + - The IOPD for a Provisioned IOPS EBS volume (SSD). + type: int + vpc_options: + description: + - Options to specify the subnets and security groups for a VPC endpoint. + type: complex + contains: + vpc_id: + description: The VPC ID for the domain. + type: str + subnet_ids: + description: + - Specifies the subnet ids for VPC endpoint. + type: list + elements: str + security_group_ids: + description: + - Specifies the security group ids for VPC endpoint. + type: list + elements: str + availability_zones: + description: + - The Availability Zones for the domain.. + type: list + elements: str + snapshot_options: + description: + - Option to set time, in UTC format, of the daily automated snapshot. + type: complex + contains: + automated_snapshot_start_hour: + description: + - > + Integer value from 0 to 23 specifying when the service takes a daily automated snapshot + of the specified Elasticsearch domain. + type: int + access_policies: + description: + - IAM access policy as a JSON-formatted string. + type: complex + encryption_at_rest_options: + description: + - Parameters to enable encryption at rest. + type: complex + contains: + enabled: + description: + - Should data be encrypted while at rest. + type: bool + kms_key_id: + description: + - If encryption at rest enabled, this identifies the encryption key to use. + - The value should be a KMS key ARN. It can also be the KMS key id. + type: str + node_to_node_encryption_options: + description: + - Node-to-node encryption options. + type: complex + contains: + enabled: + description: + - True to enable node-to-node encryption. + type: bool + cognito_options: + description: + - Parameters to configure OpenSearch Service to use Amazon Cognito authentication for OpenSearch Dashboards. + type: complex + contains: + enabled: + description: + - The option to enable Cognito for OpenSearch Dashboards authentication. + type: bool + user_pool_id: + description: + - The Cognito user pool ID for OpenSearch Dashboards authentication. + type: str + identity_pool_id: + description: + - The Cognito identity pool ID for OpenSearch Dashboards authentication. + type: str + role_arn: + description: + - The role ARN that provides OpenSearch permissions for accessing Cognito resources. + type: str + domain_endpoint_options: + description: + - Options to specify configuration that will be applied to the domain endpoint. + type: complex + contains: + enforce_https: + description: + - Whether only HTTPS endpoint should be enabled for the domain. + type: bool + tls_security_policy: + description: + - Specify the TLS security policy to apply to the HTTPS endpoint of the domain. + type: str + custom_endpoint_enabled: + description: + - Whether to enable a custom endpoint for the domain. + type: bool + custom_endpoint: + description: + - The fully qualified domain for your custom endpoint. + type: str + custom_endpoint_certificate_arn: + description: + - The ACM certificate ARN for your custom endpoint. + type: str + advanced_security_options: + description: + - Specifies advanced security options. + type: complex + contains: + enabled: + description: + - True if advanced security is enabled. + - You must enable node-to-node encryption to use advanced security options. + type: bool + internal_user_database_enabled: + description: + - True if the internal user database is enabled. + type: bool + master_user_options: + description: + - Credentials for the master user, username and password, ARN, or both. + type: complex + contains: + master_user_arn: + description: + - ARN for the master user (if IAM is enabled). + type: str + master_user_name: + description: + - The username of the master user, which is stored in the Amazon OpenSearch Service domain internal database. + type: str + master_user_password: + description: + - The password of the master user, which is stored in the Amazon OpenSearch Service domain internal database. + type: str + saml_options: + description: + - The SAML application configuration for the domain. + type: complex + contains: + enabled: + description: + - True if SAML is enabled. + type: bool + idp: + description: + - The SAML Identity Provider's information. + type: complex + contains: + metadata_content: + description: + - The metadata of the SAML application in XML format. + type: str + entity_id: + description: + - The unique entity ID of the application in SAML identity provider. + type: str + master_user_name: + description: + - The SAML master username, which is stored in the Amazon OpenSearch Service domain internal database. + type: str + master_backend_role: + description: + - The backend role that the SAML master user is mapped to. + type: str + subject_key: + description: + - Element of the SAML assertion to use for username. Default is NameID. + type: str + roles_key: + description: + - Element of the SAML assertion to use for backend roles. Default is roles. + type: str + session_timeout_minutes: + description: + - > + The duration, in minutes, after which a user session becomes inactive. + Acceptable values are between 1 and 1440, and the default value is 60. + type: int + auto_tune_options: + description: + - Specifies Auto-Tune options. + type: complex + contains: + desired_state: + description: + - The Auto-Tune desired state. Valid values are ENABLED and DISABLED. + type: str + maintenance_schedules: + description: + - A list of maintenance schedules. + type: list + elements: dict + contains: + start_at: + description: + - The timestamp at which the Auto-Tune maintenance schedule starts. + type: str + duration: + description: + - Specifies maintenance schedule duration, duration value and duration unit. + type: complex + contains: + value: + description: + - Integer to specify the value of a maintenance schedule duration. + type: int + unit: + description: + - The unit of a maintenance schedule duration. Valid value is HOURS. + type: str + cron_expression_for_recurrence: + description: + - A cron expression for a recurring maintenance schedule. + type: str + domain_config: + description: The OpenSearch domain configuration + returned: always + type: complex + contains: + domain_name: + description: The name of the OpenSearch domain. + returned: always + type: str +''' + + +try: + import botocore +except ImportError: + pass # handled by AnsibleAWSModule + +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ( + AWSRetry, + boto3_tag_list_to_ansible_dict, + camel_dict_to_snake_dict, +) +from ansible_collections.community.aws.plugins.module_utils.opensearch import ( + get_domain_config, + get_domain_status, +) + + +def domain_info(client, module): + domain_name = module.params.get('domain_name') + filter_tags = module.params.get('tags') + + domain_list = [] + if domain_name: + domain_status = get_domain_status(client, module, domain_name) + if domain_status: + domain_list.append({'DomainStatus': domain_status}) + else: + domain_summary_list = client.list_domain_names()['DomainNames'] + for d in domain_summary_list: + domain_status = get_domain_status(client, module, d['DomainName']) + if domain_status: + domain_list.append({'DomainStatus': domain_status}) + + # Get the domain tags + for domain in domain_list: + current_domain_tags = None + domain_arn = domain['DomainStatus']['ARN'] + try: + current_domain_tags = client.list_tags(ARN=domain_arn, aws_retry=True)["TagList"] + domain['Tags'] = boto3_tag_list_to_ansible_dict(current_domain_tags) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + # This could potentially happen if a domain is deleted between the time + # its domain status was queried and the tags were queried. + domain['Tags'] = {} + + # Filter by tags + if filter_tags: + for tag_key in filter_tags: + try: + domain_list = [c for c in domain_list if ('Tags' in c) and (tag_key in c['Tags']) and (c['Tags'][tag_key] == filter_tags[tag_key])] + except (TypeError, AttributeError) as e: + module.fail_json(msg="OpenSearch tag filtering error", exception=e) + + # Get the domain config + for idx, domain in enumerate(domain_list): + domain_name = domain['DomainStatus']['DomainName'] + (domain_config, arn) = get_domain_config(client, module, domain_name) + if domain_config: + domain['DomainConfig'] = domain_config + domain_list[idx] = camel_dict_to_snake_dict(domain, + ignore_list=['AdvancedOptions', 'Endpoints', 'Tags']) + + return dict(changed=False, domains=domain_list) + + +def main(): + module = AnsibleAWSModule( + argument_spec=dict( + domain_name=dict(required=False), + tags=dict(type='dict', required=False), + ), + supports_check_mode=True, + ) + module.require_botocore_at_least("1.21.38") + + try: + client = module.client("opensearch", 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 opensearch service") + + module.exit_json(**domain_info(client, module)) + + +if __name__ == '__main__': + main()