#!/usr/bin/python
# 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 = r'''
---
module: rds
version_added: 1.0.0
short_description: create, delete, or modify Amazon rds instances, rds snapshots, and related facts
description:
    - Creates, deletes, or modifies rds resources.
    - When creating an instance it can be either a new instance or a read-only replica of an existing instance.
    - This module has a dependency on python-boto >= 2.5 and will soon be deprecated.
    - The 'promote' command requires boto >= 2.18.0. Certain features such as tags rely on boto.rds2 (boto >= 2.26.0).
    - Please use boto3 based M(community.aws.rds_instance) instead.
options:
  command:
    description:
      - Specifies the action to take. The 'reboot' option is available starting at version 2.0.
    required: true
    choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'reboot', 'restore' ]
    type: str
  instance_name:
    description:
      - Database instance identifier.
      - Required except when using I(command=facts) or I(command=delete) on just a snapshot.
    type: str
  source_instance:
    description:
      - Name of the database to replicate.
      - Used only when I(command=replicate).
    type: str
  db_engine:
    description:
      - The type of database.
      - Used only when I(command=create).
      - mariadb was added in version 2.2.
    choices: ['mariadb', 'MySQL', 'oracle-se1', 'oracle-se2', 'oracle-se', 'oracle-ee',
              'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres', 'aurora']
    type: str
  size:
    description:
      - Size in gigabytes of the initial storage for the DB instance.
      - Used only when I(command=create) or I(command=modify).
    type: str
  instance_type:
    description:
      - The instance type of the database.
      - If not specified then the replica inherits the same instance type as the source instance.
      - Required when I(command=create).
      - Optional when I(command=replicate), I(command=modify) or I(command=restore).
    aliases: ['type']
    type: str
  username:
    description:
      - Master database username.
      - Used only when I(command=create).
    type: str
  password:
    description:
      - Password for the master database username.
      - Used only when I(command=create) or I(command=modify).
    type: str
  db_name:
    description:
      - Name of a database to create within the instance.
      - If not specified then no database is created.
      - Used only when I(command=create).
    type: str
  engine_version:
    description:
      - Version number of the database engine to use.
      - If not specified then the current Amazon RDS default engine version is used
      - Used only when I(command=create).
    type: str
  parameter_group:
    description:
      - Name of the DB parameter group to associate with this instance.
      - If omitted then the RDS default DBParameterGroup will be used.
      - Used only when I(command=create) or I(command=modify).
    type: str
  license_model:
    description:
      - The license model for this DB instance.
      - Used only when I(command=create) or I(command=restore).
    choices:  [ 'license-included', 'bring-your-own-license', 'general-public-license', 'postgresql-license' ]
    type: str
  multi_zone:
    description:
      - Specifies if this is a Multi-availability-zone deployment.
      - Can not be used in conjunction with I(zone) parameter.
      - Used only when I(command=create) or I(command=modify).
    type: bool
  iops:
    description:
      - Specifies the number of IOPS for the instance.
      - Used only when I(command=create) or I(command=modify).
      - Must be an integer greater than 1000.
    type: str
  security_groups:
    description:
      - Comma separated list of one or more security groups.
      - Used only when I(command=create) or I(command=modify).
    type: str
  vpc_security_groups:
    description:
      - Comma separated list of one or more vpc security group ids.
      - Also requires I(subnet) to be specified.
      - Used only when I(command=create) or I(command=modify).
    type: list
    elements: str
  port:
    description:
      - Port number that the DB instance uses for connections.
      - Used only when I(command=create) or I(command=replicate).
      - 'Defaults to the standard ports for each I(db_engine): C(3306) for MySQL and MariaDB, C(1521) for Oracle
        C(1433) for SQL Server, C(5432) for PostgreSQL.'
    type: int
  upgrade:
    description:
      - Indicates that minor version upgrades should be applied automatically.
      - Used only when I(command=create) or I(command=modify) or I(command=restore) or I(command=replicate).
    type: bool
    default: false
  option_group:
    description:
      - The name of the option group to use.
      - If not specified then the default option group is used.
      - Used only when I(command=create).
    type: str
  maint_window:
    description:
      - 'Maintenance window in format of C(ddd:hh24:mi-ddd:hh24:mi).  (Example: C(Mon:22:00-Mon:23:15))'
      - Times are specified in UTC.
      - If not specified then a random maintenance window is assigned.
      - Used only when I(command=create) or I(command=modify).
    type: str
  backup_window:
    description:
      - 'Backup window in format of C(hh24:mi-hh24:mi). (Example: C(18:00-20:30))'
      - Times are specified in UTC.
      - If not specified then a random backup window is assigned.
      - Used only when command=create or command=modify.
    type: str
  backup_retention:
    description:
      - Number of days backups are retained.
      - Set to 0 to disable backups.
      - Default is 1 day.
      - 'Valid range: 0-35.'
      - Used only when I(command=create) or I(command=modify).
    type: str
  zone:
    description:
      - availability zone in which to launch the instance.
      - Used only when I(command=create), I(command=replicate) or I(command=restore).
      - Can not be used in conjunction with I(multi_zone) parameter.
    aliases: ['aws_zone', 'ec2_zone']
    type: str
  subnet:
    description:
      - VPC subnet group.
      - If specified then a VPC instance is created.
      - Used only when I(command=create).
    type: str
  snapshot:
    description:
      - Name of snapshot to take.
      - When I(command=delete), if no I(snapshot) name is provided then no snapshot is taken.
      - When I(command=delete), if no I(instance_name) is provided the snapshot is deleted.
      - Used with I(command=facts), I(command=delete) or I(command=snapshot).
    type: str
  wait:
    description:
      - When I(command=create), replicate, modify or restore then wait for the database to enter the 'available' state.
      - When I(command=delete), wait for the database to be terminated.
    type: bool
    default: false
  wait_timeout:
    description:
      - How long before wait gives up, in seconds.
      - Used when I(wait=true).
    default: 300
    type: int
  apply_immediately:
    description:
      - When I(apply_immediately=true), the modifications will be applied as soon as possible rather than waiting for the
        next preferred maintenance window.
      - Used only when I(command=modify).
    type: bool
    default: false
  force_failover:
    description:
      - If enabled, the reboot is done using a MultiAZ failover.
      - Used only when I(command=reboot).
    type: bool
    default: false
  new_instance_name:
    description:
      - Name to rename an instance to.
      - Used only when I(command=modify).
    type: str
  character_set_name:
    description:
      - Associate the DB instance with a specified character set.
      - Used with I(command=create).
    type: str
  publicly_accessible:
    description:
      - Explicitly set whether the resource should be publicly accessible or not.
      - Used with I(command=create), I(command=replicate).
      - Requires boto >= 2.26.0
    type: str
  tags:
    description:
      - tags dict to apply to a resource.
      - Used with I(command=create), I(command=replicate), I(command=restore).
      - Requires boto >= 2.26.0
    type: dict
requirements:
    - "python >= 2.6"
    - "boto"
author:
    - "Bruce Pennypacker (@bpennypacker)"
    - "Will Thames (@willthames)"
extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2

'''

# FIXME: the command stuff needs a 'state' like alias to make things consistent -- MPD

EXAMPLES = r'''
- name: Basic mysql provisioning example
  community.aws.rds:
    command: create
    instance_name: new-database
    db_engine: MySQL
    size: 10
    instance_type: db.m1.small
    username: mysql_admin
    password: 1nsecure
    tags:
      Environment: testing
      Application: cms

- name: Create a read-only replica and wait for it to become available
  community.aws.rds:
    command: replicate
    instance_name: new-database-replica
    source_instance: new_database
    wait: yes
    wait_timeout: 600

- name: Delete an instance, but create a snapshot before doing so
  community.aws.rds:
    command: delete
    instance_name: new-database
    snapshot: new_database_snapshot

- name: Get facts about an instance
  community.aws.rds:
    command: facts
    instance_name: new-database
  register: new_database_facts

- name: Rename an instance and wait for the change to take effect
  community.aws.rds:
    command: modify
    instance_name: new-database
    new_instance_name: renamed-database
    wait: yes

- name: Reboot an instance and wait for it to become available again
  community.aws.rds:
    command: reboot
    instance_name: database
    wait: yes

# Restore a Postgres db instance from a snapshot, wait for it to become available again, and
#  then modify it to add your security group. Also, display the new endpoint.
#  Note that the "publicly_accessible" option is allowed here just as it is in the AWS CLI
- community.aws.rds:
    command: restore
    snapshot: mypostgres-snapshot
    instance_name: MyNewInstanceName
    region: us-west-2
    zone: us-west-2b
    subnet: default-vpc-xx441xxx
    publicly_accessible: yes
    wait: yes
    wait_timeout: 600
    tags:
        Name: pg1_test_name_tag
  register: rds

- community.aws.rds:
    command: modify
    instance_name: MyNewInstanceName
    region: us-west-2
    vpc_security_groups: sg-xxx945xx

- ansible.builtin.debug:
    msg: "The new db endpoint is {{ rds.instance.endpoint }}"
'''

RETURN = r'''
instance:
    description: the rds instance
    returned: always
    type: complex
    contains:
        engine:
            description: the name of the database engine
            returned: when RDS instance exists
            type: str
            sample: "oracle-se"
        engine_version:
            description: the version of the database engine
            returned: when RDS instance exists
            type: str
            sample: "11.2.0.4.v6"
        license_model:
            description: the license model information
            returned: when RDS instance exists
            type: str
            sample: "bring-your-own-license"
        character_set_name:
            description: the name of the character set that this instance is associated with
            returned: when RDS instance exists
            type: str
            sample: "AL32UTF8"
        allocated_storage:
            description: the allocated storage size in gigabytes (GB)
            returned: when RDS instance exists
            type: str
            sample: "100"
        publicly_accessible:
            description: the accessibility options for the DB instance
            returned: when RDS instance exists
            type: bool
            sample: "true"
        latest_restorable_time:
            description: the latest time to which a database can be restored with point-in-time restore
            returned: when RDS instance exists
            type: str
            sample: "1489707802.0"
        secondary_availability_zone:
            description: the name of the secondary AZ for a DB instance with multi-AZ support
            returned: when RDS instance exists and is multi-AZ
            type: str
            sample: "eu-west-1b"
        backup_window:
            description: the daily time range during which automated backups are created if automated backups are enabled
            returned: when RDS instance exists and automated backups are enabled
            type: str
            sample: "03:00-03:30"
        auto_minor_version_upgrade:
            description: indicates that minor engine upgrades will be applied automatically to the DB instance during the maintenance window
            returned: when RDS instance exists
            type: bool
            sample: "true"
        read_replica_source_dbinstance_identifier:
            description: the identifier of the source DB instance if this RDS instance is a read replica
            returned: when read replica RDS instance exists
            type: str
            sample: "null"
        db_name:
            description: the name of the database to create when the DB instance is created
            returned: when RDS instance exists
            type: str
            sample: "ASERTG"
        endpoint:
            description: the endpoint uri of the database instance
            returned: when RDS instance exists
            type: str
            sample: "my-ansible-database.asdfaosdgih.us-east-1.rds.amazonaws.com"
        port:
            description: the listening port of the database instance
            returned: when RDS instance exists
            type: int
            sample: 3306
        parameter_groups:
            description: the list of DB parameter groups applied to this RDS instance
            returned: when RDS instance exists and parameter groups are defined
            type: complex
            contains:
                parameter_apply_status:
                    description: the status of parameter updates
                    returned: when RDS instance exists
                    type: str
                    sample: "in-sync"
                parameter_group_name:
                    description: the name of the DP parameter group
                    returned: when RDS instance exists
                    type: str
                    sample: "testawsrpprodb01spfile-1ujg7nrs7sgyz"
        option_groups:
            description: the list of option group memberships for this RDS instance
            returned: when RDS instance exists
            type: complex
            contains:
                option_group_name:
                    description: the option group name for this RDS instance
                    returned: when RDS instance exists
                    type: str
                    sample: "default:oracle-se-11-2"
                status:
                    description: the status of the RDS instance's option group membership
                    returned: when RDS instance exists
                    type: str
                    sample: "in-sync"
        pending_modified_values:
            description: a dictionary of changes to the RDS instance that are pending
            returned: when RDS instance exists
            type: complex
            contains:
                db_instance_class:
                    description: the new DB instance class for this RDS instance that will be applied or is in progress
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                db_instance_identifier:
                    description: the new DB instance identifier this RDS instance that will be applied or is in progress
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                allocated_storage:
                    description: the new allocated storage size for this RDS instance that will be applied or is in progress
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                backup_retention_period:
                    description: the pending number of days for which automated backups are retained
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                engine_version:
                    description: indicates the database engine version
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                iops:
                    description: the new provisioned IOPS value for this RDS instance that will be applied or is being applied
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                master_user_password:
                    description: the pending or in-progress change of the master credentials for this RDS instance
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                multi_az:
                    description: indicates that the single-AZ RDS instance is to change to a multi-AZ deployment
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
                port:
                    description: specifies the pending port for this RDS instance
                    returned: when RDS instance exists
                    type: str
                    sample: "null"
        db_subnet_groups:
            description: information on the subnet group associated with this RDS instance
            returned: when RDS instance exists
            type: complex
            contains:
                description:
                    description: the subnet group associated with the DB instance
                    returned: when RDS instance exists
                    type: str
                    sample: "Subnets for the UAT RDS SQL DB Instance"
                name:
                    description: the name of the DB subnet group
                    returned: when RDS instance exists
                    type: str
                    sample: "samplesubnetgrouprds-j6paiqkxqp4z"
                status:
                    description: the status of the DB subnet group
                    returned: when RDS instance exists
                    type: str
                    sample: "complete"
                subnets:
                    description: the description of the DB subnet group
                    returned: when RDS instance exists
                    type: complex
                    contains:
                        availability_zone:
                            description: subnet availability zone information
                            returned: when RDS instance exists
                            type: complex
                            contains:
                                name:
                                    description: availability zone
                                    returned: when RDS instance exists
                                    type: str
                                    sample: "eu-west-1b"
                                provisioned_iops_capable:
                                    description: whether provisioned iops are available in AZ subnet
                                    returned: when RDS instance exists
                                    type: bool
                                    sample: "false"
                        identifier:
                            description: the identifier of the subnet
                            returned: when RDS instance exists
                            type: str
                            sample: "subnet-3fdba63e"
                        status:
                            description: the status of the subnet
                            returned: when RDS instance exists
                            type: str
                            sample: "active"
'''

import time

try:
    import boto.rds
    import boto.exception
except ImportError:
    pass  # Taken care of by ec2.HAS_BOTO

try:
    import boto.rds2
    import boto.rds2.exceptions
    HAS_RDS2 = True
except ImportError:
    HAS_RDS2 = False

from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import HAS_BOTO
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import connect_to_aws
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_aws_connection_info


DEFAULT_PORTS = {
    'aurora': 3306,
    'mariadb': 3306,
    'mysql': 3306,
    'oracle': 1521,
    'sqlserver': 1433,
    'postgres': 5432,
}


class RDSException(Exception):
    def __init__(self, exc):
        if hasattr(exc, 'error_message') and exc.error_message:
            self.message = exc.error_message
            self.code = exc.error_code
        elif hasattr(exc, 'body') and 'Error' in exc.body:
            self.message = exc.body['Error']['Message']
            self.code = exc.body['Error']['Code']
        else:
            self.message = str(exc)
            self.code = 'Unknown Error'


class RDSConnection:
    def __init__(self, module, region, **aws_connect_params):
        try:
            self.connection = connect_to_aws(boto.rds, region, **aws_connect_params)
        except boto.exception.BotoServerError as e:
            module.fail_json(msg=e.error_message)

    def get_db_instance(self, instancename):
        try:
            return RDSDBInstance(self.connection.get_all_dbinstances(instancename)[0])
        except boto.exception.BotoServerError:
            return None

    def get_db_snapshot(self, snapshotid):
        try:
            return RDSSnapshot(self.connection.get_all_dbsnapshots(snapshot_id=snapshotid)[0])
        except boto.exception.BotoServerError:
            return None

    def create_db_instance(self, instance_name, size, instance_class, db_engine,
                           username, password, **params):
        params['engine'] = db_engine
        try:
            result = self.connection.create_dbinstance(instance_name, size, instance_class,
                                                       username, password, **params)
            return RDSDBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def create_db_instance_read_replica(self, instance_name, source_instance, **params):
        try:
            result = self.connection.createdb_instance_read_replica(instance_name, source_instance, **params)
            return RDSDBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def delete_db_instance(self, instance_name, **params):
        try:
            result = self.connection.delete_dbinstance(instance_name, **params)
            return RDSDBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def delete_db_snapshot(self, snapshot):
        try:
            result = self.connection.delete_dbsnapshot(snapshot)
            return RDSSnapshot(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def modify_db_instance(self, instance_name, **params):
        try:
            result = self.connection.modify_dbinstance(instance_name, **params)
            return RDSDBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def reboot_db_instance(self, instance_name, **params):
        try:
            result = self.connection.reboot_dbinstance(instance_name)
            return RDSDBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def restore_db_instance_from_db_snapshot(self, instance_name, snapshot, instance_type, **params):
        try:
            result = self.connection.restore_dbinstance_from_dbsnapshot(snapshot, instance_name, instance_type, **params)
            return RDSDBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def create_db_snapshot(self, snapshot, instance_name, **params):
        try:
            result = self.connection.create_dbsnapshot(snapshot, instance_name)
            return RDSSnapshot(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def promote_read_replica(self, instance_name, **params):
        try:
            result = self.connection.promote_read_replica(instance_name, **params)
            return RDSDBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)


class RDS2Connection:
    def __init__(self, module, region, **aws_connect_params):
        try:
            self.connection = connect_to_aws(boto.rds2, region, **aws_connect_params)
        except boto.exception.BotoServerError as e:
            module.fail_json(msg=e.error_message)

    def get_db_instance(self, instancename):
        try:
            dbinstances = self.connection.describe_db_instances(
                db_instance_identifier=instancename
            )['DescribeDBInstancesResponse']['DescribeDBInstancesResult']['DBInstances']
            result = RDS2DBInstance(dbinstances[0])
            return result
        except boto.rds2.exceptions.DBInstanceNotFound as e:
            return None
        except Exception as e:
            raise e

    def get_db_snapshot(self, snapshotid):
        try:
            snapshots = self.connection.describe_db_snapshots(
                db_snapshot_identifier=snapshotid,
                snapshot_type='manual'
            )['DescribeDBSnapshotsResponse']['DescribeDBSnapshotsResult']['DBSnapshots']
            result = RDS2Snapshot(snapshots[0])
            return result
        except boto.rds2.exceptions.DBSnapshotNotFound:
            return None

    def create_db_instance(self, instance_name, size, instance_class, db_engine,
                           username, password, **params):
        try:
            result = self.connection.create_db_instance(instance_name, size, instance_class, db_engine, username, password,
                                                        **params)['CreateDBInstanceResponse']['CreateDBInstanceResult']['DBInstance']
            return RDS2DBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def create_db_instance_read_replica(self, instance_name, source_instance, **params):
        try:
            result = self.connection.create_db_instance_read_replica(
                instance_name,
                source_instance,
                **params
            )['CreateDBInstanceReadReplicaResponse']['CreateDBInstanceReadReplicaResult']['DBInstance']
            return RDS2DBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def delete_db_instance(self, instance_name, **params):
        try:
            result = self.connection.delete_db_instance(instance_name, **params)['DeleteDBInstanceResponse']['DeleteDBInstanceResult']['DBInstance']
            return RDS2DBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def delete_db_snapshot(self, snapshot):
        try:
            result = self.connection.delete_db_snapshot(snapshot)['DeleteDBSnapshotResponse']['DeleteDBSnapshotResult']['DBSnapshot']
            return RDS2Snapshot(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def modify_db_instance(self, instance_name, **params):
        try:
            result = self.connection.modify_db_instance(instance_name, **params)['ModifyDBInstanceResponse']['ModifyDBInstanceResult']['DBInstance']
            return RDS2DBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def reboot_db_instance(self, instance_name, **params):
        try:
            result = self.connection.reboot_db_instance(instance_name, **params)['RebootDBInstanceResponse']['RebootDBInstanceResult']['DBInstance']
            return RDS2DBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def restore_db_instance_from_db_snapshot(self, instance_name, snapshot, instance_type, **params):
        try:
            result = self.connection.restore_db_instance_from_db_snapshot(
                instance_name,
                snapshot,
                **params
            )['RestoreDBInstanceFromDBSnapshotResponse']['RestoreDBInstanceFromDBSnapshotResult']['DBInstance']
            return RDS2DBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def create_db_snapshot(self, snapshot, instance_name, **params):
        try:
            result = self.connection.create_db_snapshot(snapshot, instance_name, **params)['CreateDBSnapshotResponse']['CreateDBSnapshotResult']['DBSnapshot']
            return RDS2Snapshot(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)

    def promote_read_replica(self, instance_name, **params):
        try:
            result = self.connection.promote_read_replica(instance_name, **params)['PromoteReadReplicaResponse']['PromoteReadReplicaResult']['DBInstance']
            return RDS2DBInstance(result)
        except boto.exception.BotoServerError as e:
            raise RDSException(e)


class RDSDBInstance:
    def __init__(self, dbinstance):
        self.instance = dbinstance
        self.name = dbinstance.id
        self.status = dbinstance.status

    def get_data(self):
        d = {
            'id': self.name,
            'create_time': self.instance.create_time,
            'status': self.status,
            'availability_zone': self.instance.availability_zone,
            'backup_retention': self.instance.backup_retention_period,
            'backup_window': self.instance.preferred_backup_window,
            'maintenance_window': self.instance.preferred_maintenance_window,
            'multi_zone': self.instance.multi_az,
            'instance_type': self.instance.instance_class,
            'username': self.instance.master_username,
            'iops': self.instance.iops
        }

        # Only assign an Endpoint if one is available
        if hasattr(self.instance, 'endpoint'):
            d["endpoint"] = self.instance.endpoint[0]
            d["port"] = self.instance.endpoint[1]
            if self.instance.vpc_security_groups is not None:
                d["vpc_security_groups"] = ','.join(x.vpc_group for x in self.instance.vpc_security_groups)
            else:
                d["vpc_security_groups"] = None
        else:
            d["endpoint"] = None
            d["port"] = None
            d["vpc_security_groups"] = None
        d['DBName'] = self.instance.DBName if hasattr(self.instance, 'DBName') else None
        # ReadReplicaSourceDBInstanceIdentifier may or may not exist
        try:
            d["replication_source"] = self.instance.ReadReplicaSourceDBInstanceIdentifier
        except Exception:
            d["replication_source"] = None
        return d


class RDS2DBInstance:
    def __init__(self, dbinstance):
        self.instance = dbinstance
        if 'DBInstanceIdentifier' not in dbinstance:
            self.name = None
        else:
            self.name = self.instance.get('DBInstanceIdentifier')
        self.status = self.instance.get('DBInstanceStatus')

    def get_data(self):
        d = {
            'id': self.name,
            'create_time': self.instance['InstanceCreateTime'],
            'engine': self.instance['Engine'],
            'engine_version': self.instance['EngineVersion'],
            'license_model': self.instance['LicenseModel'],
            'character_set_name': self.instance['CharacterSetName'],
            'allocated_storage': self.instance['AllocatedStorage'],
            'publicly_accessible': self.instance['PubliclyAccessible'],
            'latest_restorable_time': self.instance['LatestRestorableTime'],
            'status': self.status,
            'availability_zone': self.instance['AvailabilityZone'],
            'secondary_availability_zone': self.instance['SecondaryAvailabilityZone'],
            'backup_retention': self.instance['BackupRetentionPeriod'],
            'backup_window': self.instance['PreferredBackupWindow'],
            'maintenance_window': self.instance['PreferredMaintenanceWindow'],
            'auto_minor_version_upgrade': self.instance['AutoMinorVersionUpgrade'],
            'read_replica_source_dbinstance_identifier': self.instance['ReadReplicaSourceDBInstanceIdentifier'],
            'multi_zone': self.instance['MultiAZ'],
            'instance_type': self.instance['DBInstanceClass'],
            'username': self.instance['MasterUsername'],
            'db_name': self.instance['DBName'],
            'iops': self.instance['Iops'],
            'replication_source': self.instance['ReadReplicaSourceDBInstanceIdentifier']
        }
        if self.instance['DBParameterGroups'] is not None:
            parameter_groups = []
            for x in self.instance['DBParameterGroups']:
                parameter_groups.append({'parameter_group_name': x['DBParameterGroupName'], 'parameter_apply_status': x['ParameterApplyStatus']})
            d['parameter_groups'] = parameter_groups
        if self.instance['OptionGroupMemberships'] is not None:
            option_groups = []
            for x in self.instance['OptionGroupMemberships']:
                option_groups.append({'status': x['Status'], 'option_group_name': x['OptionGroupName']})
            d['option_groups'] = option_groups
        if self.instance['PendingModifiedValues'] is not None:
            pdv = self.instance['PendingModifiedValues']
            d['pending_modified_values'] = {
                'multi_az': pdv['MultiAZ'],
                'master_user_password': pdv['MasterUserPassword'],
                'port': pdv['Port'],
                'iops': pdv['Iops'],
                'allocated_storage': pdv['AllocatedStorage'],
                'engine_version': pdv['EngineVersion'],
                'backup_retention_period': pdv['BackupRetentionPeriod'],
                'db_instance_class': pdv['DBInstanceClass'],
                'db_instance_identifier': pdv['DBInstanceIdentifier']
            }
        if self.instance["DBSubnetGroup"] is not None:
            dsg = self.instance["DBSubnetGroup"]
            db_subnet_groups = {}
            db_subnet_groups['vpc_id'] = dsg['VpcId']
            db_subnet_groups['name'] = dsg['DBSubnetGroupName']
            db_subnet_groups['status'] = dsg['SubnetGroupStatus'].lower()
            db_subnet_groups['description'] = dsg['DBSubnetGroupDescription']
            db_subnet_groups['subnets'] = []
            for x in dsg["Subnets"]:
                db_subnet_groups['subnets'].append({
                    'status': x['SubnetStatus'].lower(),
                    'identifier': x['SubnetIdentifier'],
                    'availability_zone': {
                        'name': x['SubnetAvailabilityZone']['Name'],
                        'provisioned_iops_capable': x['SubnetAvailabilityZone']['ProvisionedIopsCapable']
                    }
                })
            d['db_subnet_groups'] = db_subnet_groups
        if self.instance["VpcSecurityGroups"] is not None:
            d['vpc_security_groups'] = ','.join(x['VpcSecurityGroupId'] for x in self.instance['VpcSecurityGroups'])
        if "Endpoint" in self.instance and self.instance["Endpoint"] is not None:
            d['endpoint'] = self.instance["Endpoint"].get('Address', None)
            d['port'] = self.instance["Endpoint"].get('Port', None)
        else:
            d['endpoint'] = None
            d['port'] = None
        d['DBName'] = self.instance['DBName'] if hasattr(self.instance, 'DBName') else None
        return d


class RDSSnapshot:
    def __init__(self, snapshot):
        self.snapshot = snapshot
        self.name = snapshot.id
        self.status = snapshot.status

    def get_data(self):
        d = {
            'id': self.name,
            'create_time': self.snapshot.snapshot_create_time,
            'status': self.status,
            'availability_zone': self.snapshot.availability_zone,
            'instance_id': self.snapshot.instance_id,
            'instance_created': self.snapshot.instance_create_time,
        }
        # needs boto >= 2.21.0
        if hasattr(self.snapshot, 'snapshot_type'):
            d["snapshot_type"] = self.snapshot.snapshot_type
        if hasattr(self.snapshot, 'iops'):
            d["iops"] = self.snapshot.iops
        return d


class RDS2Snapshot:
    def __init__(self, snapshot):
        if 'DeleteDBSnapshotResponse' in snapshot:
            self.snapshot = snapshot['DeleteDBSnapshotResponse']['DeleteDBSnapshotResult']['DBSnapshot']
        else:
            self.snapshot = snapshot
        self.name = self.snapshot.get('DBSnapshotIdentifier')
        self.status = self.snapshot.get('Status')

    def get_data(self):
        d = {
            'id': self.name,
            'create_time': self.snapshot['SnapshotCreateTime'],
            'status': self.status,
            'availability_zone': self.snapshot['AvailabilityZone'],
            'instance_id': self.snapshot['DBInstanceIdentifier'],
            'instance_created': self.snapshot['InstanceCreateTime'],
            'snapshot_type': self.snapshot['SnapshotType'],
            'iops': self.snapshot['Iops'],
        }
        return d


def await_resource(conn, resource, status, module):
    start_time = time.time()
    wait_timeout = module.params.get('wait_timeout') + start_time
    check_interval = 5
    while wait_timeout > time.time() and resource.status != status:
        time.sleep(check_interval)
        if wait_timeout <= time.time():
            module.fail_json(msg="Timeout waiting for RDS resource %s" % resource.name)
        if module.params.get('command') == 'snapshot':
            # Temporary until all the rds2 commands have their responses parsed
            if resource.name is None:
                module.fail_json(msg="There was a problem waiting for RDS snapshot %s" % resource.snapshot)
            # Back off if we're getting throttled, since we're just waiting anyway
            resource = AWSRetry.backoff(tries=5, delay=20, backoff=1.5)(conn.get_db_snapshot)(resource.name)
        else:
            # Temporary until all the rds2 commands have their responses parsed
            if resource.name is None:
                module.fail_json(msg="There was a problem waiting for RDS instance %s" % resource.instance)
            # Back off if we're getting throttled, since we're just waiting anyway
            resource = AWSRetry.backoff(tries=5, delay=20, backoff=1.5)(conn.get_db_instance)(resource.name)
            if resource is None:
                break
        # Some RDS resources take much longer than others to be ready. Check
        # less aggressively for slow ones to avoid throttling.
        if time.time() > start_time + 90:
            check_interval = 20
    return resource


def create_db_instance(module, conn):
    required_vars = ['instance_name', 'db_engine', 'size', 'instance_type', 'username', 'password']
    valid_vars = ['backup_retention', 'backup_window',
                  'character_set_name', 'db_name', 'engine_version',
                  'instance_type', 'iops', 'license_model', 'maint_window',
                  'multi_zone', 'option_group', 'parameter_group', 'port',
                  'subnet', 'upgrade', 'zone']
    if module.params.get('subnet'):
        valid_vars.append('vpc_security_groups')
    else:
        valid_vars.append('security_groups')
    if HAS_RDS2:
        valid_vars.extend(['publicly_accessible', 'tags'])
    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')

    result = conn.get_db_instance(instance_name)
    if result:
        changed = False
    else:
        try:
            result = conn.create_db_instance(instance_name, module.params.get('size'),
                                             module.params.get('instance_type'), module.params.get('db_engine'),
                                             module.params.get('username'), module.params.get('password'), **params)
            changed = True
        except RDSException as e:
            module.fail_json(msg="Failed to create instance: %s" % e.message)

    if module.params.get('wait'):
        resource = await_resource(conn, result, 'available', module)
    else:
        resource = conn.get_db_instance(instance_name)

    module.exit_json(changed=changed, instance=resource.get_data())


def replicate_db_instance(module, conn):
    required_vars = ['instance_name', 'source_instance']
    valid_vars = ['instance_type', 'port', 'upgrade', 'zone']
    if HAS_RDS2:
        valid_vars.extend(['iops', 'option_group', 'publicly_accessible', 'tags'])
    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')
    source_instance = module.params.get('source_instance')

    result = conn.get_db_instance(instance_name)
    if result:
        changed = False
    else:
        try:
            result = conn.create_db_instance_read_replica(instance_name, source_instance, **params)
            changed = True
        except RDSException as e:
            module.fail_json(msg="Failed to create replica instance: %s " % e.message)

    if module.params.get('wait'):
        resource = await_resource(conn, result, 'available', module)
    else:
        resource = conn.get_db_instance(instance_name)

    module.exit_json(changed=changed, instance=resource.get_data())


def delete_db_instance_or_snapshot(module, conn):
    required_vars = []
    valid_vars = ['instance_name', 'snapshot', 'skip_final_snapshot']
    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')
    snapshot = module.params.get('snapshot')

    if not instance_name:
        result = conn.get_db_snapshot(snapshot)
    else:
        result = conn.get_db_instance(instance_name)
    if not result:
        module.exit_json(changed=False)
    if result.status == 'deleting':
        module.exit_json(changed=False)
    try:
        if instance_name:
            if snapshot:
                params["skip_final_snapshot"] = False
                if HAS_RDS2:
                    params["final_db_snapshot_identifier"] = snapshot
                else:
                    params["final_snapshot_id"] = snapshot
            else:
                params["skip_final_snapshot"] = True
            result = conn.delete_db_instance(instance_name, **params)
        else:
            result = conn.delete_db_snapshot(snapshot)
    except RDSException as e:
        module.fail_json(msg="Failed to delete instance: %s" % e.message)

    # If we're not waiting for a delete to complete then we're all done
    # so just return
    if not module.params.get('wait'):
        module.exit_json(changed=True)
    try:
        await_resource(conn, result, 'deleted', module)
        module.exit_json(changed=True)
    except RDSException as e:
        if e.code == 'DBInstanceNotFound':
            module.exit_json(changed=True)
        else:
            module.fail_json(msg=e.message)
    except Exception as e:
        module.fail_json(msg=str(e))


def facts_db_instance_or_snapshot(module, conn):
    instance_name = module.params.get('instance_name')
    snapshot = module.params.get('snapshot')

    if instance_name and snapshot:
        module.fail_json(msg="Facts must be called with either instance_name or snapshot, not both")
    if instance_name:
        resource = conn.get_db_instance(instance_name)
        if not resource:
            module.fail_json(msg="DB instance %s does not exist" % instance_name)
    if snapshot:
        resource = conn.get_db_snapshot(snapshot)
        if not resource:
            module.fail_json(msg="DB snapshot %s does not exist" % snapshot)

    module.exit_json(changed=False, instance=resource.get_data())


def modify_db_instance(module, conn):
    required_vars = ['instance_name']
    valid_vars = ['apply_immediately', 'backup_retention', 'backup_window',
                  'db_name', 'engine_version', 'instance_type', 'iops', 'license_model',
                  'maint_window', 'multi_zone', 'new_instance_name',
                  'option_group', 'parameter_group', 'password', 'size', 'upgrade']

    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')
    new_instance_name = module.params.get('new_instance_name')

    try:
        result = conn.modify_db_instance(instance_name, **params)
    except RDSException as e:
        module.fail_json(msg=e.message)
    if params.get('apply_immediately'):
        if new_instance_name:
            # Wait until the new instance name is valid
            new_instance = None
            while not new_instance:
                new_instance = conn.get_db_instance(new_instance_name)
                time.sleep(5)

            # Found instance but it briefly flicks to available
            # before rebooting so let's wait until we see it rebooting
            # before we check whether to 'wait'
            result = await_resource(conn, new_instance, 'rebooting', module)

    if module.params.get('wait'):
        resource = await_resource(conn, result, 'available', module)
    else:
        resource = conn.get_db_instance(instance_name)

    # guess that this changed the DB, need a way to check
    module.exit_json(changed=True, instance=resource.get_data())


def promote_db_instance(module, conn):
    required_vars = ['instance_name']
    valid_vars = ['backup_retention', 'backup_window']
    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')

    result = conn.get_db_instance(instance_name)
    if not result:
        module.fail_json(msg="DB Instance %s does not exist" % instance_name)

    if result.get_data().get('replication_source'):
        try:
            result = conn.promote_read_replica(instance_name, **params)
            changed = True
        except RDSException as e:
            module.fail_json(msg=e.message)
    else:
        changed = False

    if module.params.get('wait'):
        resource = await_resource(conn, result, 'available', module)
    else:
        resource = conn.get_db_instance(instance_name)

    module.exit_json(changed=changed, instance=resource.get_data())


def snapshot_db_instance(module, conn):
    required_vars = ['instance_name', 'snapshot']
    valid_vars = ['tags']
    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')
    snapshot = module.params.get('snapshot')
    changed = False
    result = conn.get_db_snapshot(snapshot)
    if not result:
        try:
            result = conn.create_db_snapshot(snapshot, instance_name, **params)
            changed = True
        except RDSException as e:
            module.fail_json(msg=e.message)

    if module.params.get('wait'):
        resource = await_resource(conn, result, 'available', module)
    else:
        resource = conn.get_db_snapshot(snapshot)

    module.exit_json(changed=changed, snapshot=resource.get_data())


def reboot_db_instance(module, conn):
    required_vars = ['instance_name']
    valid_vars = []

    if HAS_RDS2:
        valid_vars.append('force_failover')

    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')
    result = conn.get_db_instance(instance_name)
    changed = False
    try:
        result = conn.reboot_db_instance(instance_name, **params)
        changed = True
    except RDSException as e:
        module.fail_json(msg=e.message)

    if module.params.get('wait'):
        resource = await_resource(conn, result, 'available', module)
    else:
        resource = conn.get_db_instance(instance_name)

    module.exit_json(changed=changed, instance=resource.get_data())


def restore_db_instance(module, conn):
    required_vars = ['instance_name', 'snapshot']
    valid_vars = ['db_name', 'iops', 'license_model', 'multi_zone',
                  'option_group', 'port', 'publicly_accessible',
                  'subnet', 'tags', 'upgrade', 'zone']
    if HAS_RDS2:
        valid_vars.append('instance_type')
    else:
        required_vars.append('instance_type')
    params = validate_parameters(required_vars, valid_vars, module)
    instance_name = module.params.get('instance_name')
    instance_type = module.params.get('instance_type')
    snapshot = module.params.get('snapshot')

    changed = False
    result = conn.get_db_instance(instance_name)
    if not result:
        try:
            result = conn.restore_db_instance_from_db_snapshot(instance_name, snapshot, instance_type, **params)
            changed = True
        except RDSException as e:
            module.fail_json(msg=e.message)

    if module.params.get('wait'):
        resource = await_resource(conn, result, 'available', module)
    else:
        resource = conn.get_db_instance(instance_name)

    module.exit_json(changed=changed, instance=resource.get_data())


def validate_parameters(required_vars, valid_vars, module):
    command = module.params.get('command')
    for v in required_vars:
        if not module.params.get(v):
            module.fail_json(msg="Parameter %s required for %s command" % (v, command))

    # map to convert rds module options to boto rds and rds2 options
    optional_params = {
        'port': 'port',
        'db_name': 'db_name',
        'zone': 'availability_zone',
        'maint_window': 'preferred_maintenance_window',
        'backup_window': 'preferred_backup_window',
        'backup_retention': 'backup_retention_period',
        'multi_zone': 'multi_az',
        'engine_version': 'engine_version',
        'upgrade': 'auto_minor_version_upgrade',
        'subnet': 'db_subnet_group_name',
        'license_model': 'license_model',
        'option_group': 'option_group_name',
        'size': 'allocated_storage',
        'iops': 'iops',
        'new_instance_name': 'new_instance_id',
        'apply_immediately': 'apply_immediately',
    }
    # map to convert rds module options to boto rds options
    optional_params_rds = {
        'db_engine': 'engine',
        'password': 'master_password',
        'parameter_group': 'param_group',
        'instance_type': 'instance_class',
    }
    # map to convert rds module options to boto rds2 options
    optional_params_rds2 = {
        'tags': 'tags',
        'publicly_accessible': 'publicly_accessible',
        'parameter_group': 'db_parameter_group_name',
        'character_set_name': 'character_set_name',
        'instance_type': 'db_instance_class',
        'password': 'master_user_password',
        'new_instance_name': 'new_db_instance_identifier',
        'force_failover': 'force_failover',
    }
    if HAS_RDS2:
        optional_params.update(optional_params_rds2)
        sec_group = 'db_security_groups'
    else:
        optional_params.update(optional_params_rds)
        sec_group = 'security_groups'
        # Check for options only supported with rds2
        for k in set(optional_params_rds2.keys()) - set(optional_params_rds.keys()):
            if module.params.get(k):
                module.fail_json(msg="Parameter %s requires boto.rds (boto >= 2.26.0)" % k)

    params = {}
    for (k, v) in optional_params.items():
        if module.params.get(k) is not None and k not in required_vars:
            if k in valid_vars:
                params[v] = module.params[k]
            else:
                if module.params.get(k) is False:
                    pass
                else:
                    module.fail_json(msg="Parameter %s is not valid for %s command" % (k, command))

    if module.params.get('security_groups'):
        params[sec_group] = module.params.get('security_groups').split(',')

    vpc_groups = module.params.get('vpc_security_groups')
    if vpc_groups:
        if HAS_RDS2:
            params['vpc_security_group_ids'] = vpc_groups
        else:
            groups_list = []
            for x in vpc_groups:
                groups_list.append(boto.rds.VPCSecurityGroupMembership(vpc_group=x))
            params['vpc_security_groups'] = groups_list

    # Convert tags dict to list of tuples that rds2 expects
    if 'tags' in params:
        params['tags'] = module.params['tags'].items()
    return params


def main():
    argument_spec = dict(
        command=dict(choices=['create', 'replicate', 'delete', 'facts', 'modify', 'promote', 'snapshot', 'reboot', 'restore'], required=True),
        instance_name=dict(required=False),
        source_instance=dict(required=False),
        db_engine=dict(choices=['mariadb', 'MySQL', 'oracle-se1', 'oracle-se2', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex',
                                'sqlserver-web', 'postgres', 'aurora'], required=False),
        size=dict(required=False),
        instance_type=dict(aliases=['type'], required=False),
        username=dict(required=False),
        password=dict(no_log=True, required=False),
        db_name=dict(required=False),
        engine_version=dict(required=False),
        parameter_group=dict(required=False),
        license_model=dict(choices=['license-included', 'bring-your-own-license', 'general-public-license', 'postgresql-license'], required=False),
        multi_zone=dict(type='bool', required=False),
        iops=dict(required=False),
        security_groups=dict(required=False),
        vpc_security_groups=dict(type='list', required=False, elements='str'),
        port=dict(required=False, type='int'),
        upgrade=dict(type='bool', default=False),
        option_group=dict(required=False),
        maint_window=dict(required=False),
        backup_window=dict(required=False),
        backup_retention=dict(required=False),
        zone=dict(aliases=['aws_zone', 'ec2_zone'], required=False),
        subnet=dict(required=False),
        wait=dict(type='bool', default=False),
        wait_timeout=dict(type='int', default=300),
        snapshot=dict(required=False),
        apply_immediately=dict(type='bool', default=False),
        new_instance_name=dict(required=False),
        tags=dict(type='dict', required=False),
        publicly_accessible=dict(required=False),
        character_set_name=dict(required=False),
        force_failover=dict(type='bool', required=False, default=False),
    )

    module = AnsibleAWSModule(
        argument_spec=argument_spec,
        check_boto3=False,
    )

    if not HAS_BOTO:
        module.fail_json(msg='boto required for this module')

    invocations = {
        'create': create_db_instance,
        'replicate': replicate_db_instance,
        'delete': delete_db_instance_or_snapshot,
        'facts': facts_db_instance_or_snapshot,
        'modify': modify_db_instance,
        'promote': promote_db_instance,
        'snapshot': snapshot_db_instance,
        'reboot': reboot_db_instance,
        'restore': restore_db_instance,
    }

    region, ec2_url, aws_connect_params = get_aws_connection_info(module)
    if not region:
        module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file")

    # set port to per db defaults if not specified
    if module.params['port'] is None and module.params['db_engine'] is not None and module.params['command'] == 'create':
        if '-' in module.params['db_engine']:
            engine = module.params['db_engine'].split('-')[0]
        else:
            engine = module.params['db_engine']
        module.params['port'] = DEFAULT_PORTS[engine.lower()]

    # connect to the rds endpoint
    if HAS_RDS2:
        conn = RDS2Connection(module, region, **aws_connect_params)
    else:
        conn = RDSConnection(module, region, **aws_connect_params)

    invocations[module.params.get('command')](module, conn)


if __name__ == '__main__':
    main()
