cloudamatic/mu

View on GitHub
modules/mu/providers/aws/database.rb

Summary

Maintainability
D
2 days
Test Coverage
## Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
#  http://egt-labs.com/mu/LICENSE.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

autoload :Net, 'net/ssh/gateway'

module MU
  class Cloud
    class AWS
      # A database as configured in {MU::Config::BasketofKittens::databases}
      class Database < MU::Cloud::Database

        # Map legal storage values for each disk type and database engine so
        # our validator can check them for us.
        STORAGE_RANGES = {
          "io1" => {
            "postgres" => 100..65536,
            "mysql" => 100..65536,
            "mariadb" => 100..65536,
            "oracle-se1" => 100..65536,
            "oracle-se2" => 100..65536,
            "oracle-se" => 100..65536,
            "oracle-ee" => 100..65536,
            "sqlserver-ex" => 100..16384,
            "sqlserver-web" => 100..16384,
            "sqlserver-ee" => 200..16384,
            "sqlserver-se" =>  200..16384
          },
          "gp2" => {
            "postgres" => 20..65536,
            "mysql" => 20..65536,
            "mariadb" => 20..65536,
            "oracle-se1" => 20..65536,
            "oracle-se2" => 20..65536,
            "oracle-se" => 20..65536,
            "oracle-ee" => 20..65536,
            "sqlserver-ex" => 20..16384,
            "sqlserver-web" => 20..16384,
            "sqlserver-ee" => 200..16384,
            "sqlserver-se" =>  200..16384
          },
          "standard" => {
            "postgres" => 5..3072,
            "mysql" => 5..3072,
            "mariadb" => 5..3072,
            "oracle-se1" => 10..3072,
            "oracle-se2" => 10..3072,
            "oracle-se" => 10..3072,
            "oracle-ee" => 10..3072,
            "sqlserver-ex" => 20..1024, # ???
            "sqlserver-web" => 20..1024, # ???
            "sqlserver-ee" => 200..4096, # ???
            "sqlserver-se" =>  200..4096 # ???
          }
        }.freeze

        # List of parameters that are legal to set in +modify_db_instance+ and +modify_db_cluster+
        MODIFIABLE = {
          "instance" => [
            :allocated_storage,
            :db_instance_class,
            :db_subnet_group_name,
            :db_security_groups,
            :vpc_security_group_ids,
            :master_user_password,
            :db_parameter_group_name,
            :backup_retention_period,
            :preferred_backup_window,
            :preferred_maintenance_window,
            :multi_az,
            :engine_version,
            :allow_major_version_upgrade,
            :auto_minor_version_upgrade,
            :license_model,
            :iops,
            :option_group_name,
            :new_db_instance_identifier,
            :storage_type,
            :tde_credential_arn,
            :tde_credential_password,
            :ca_certificate_identifier,
            :domain,
            :copy_tags_to_snapshot,
            :monitoring_interval,
            :db_port_number,
            :publicly_accessible,
            :monitoring_role_arn,
            :domain_iam_role_name,
            :promotion_tier,
            :enable_iam_database_authentication,
            :enable_performance_insights,
            :performance_insights_kms_key_id,
            :performance_insights_retention_period,
            :cloudwatch_logs_export_configuration,
            :processor_features,
            :use_default_processor_features,
            :deletion_protection,
            :max_allocated_storage,
            :certificate_rotation_restart
          ],
          "cluster" => [
            :new_db_cluster_identifier,
            :backup_retention_period,
            :db_cluster_parameter_group_name,
            :vpc_security_group_ids,
            :port,
            :master_user_password,
            :option_group_name,
            :preferred_backup_window,
            :preferred_maintenance_window,
            :enable_iam_database_authentication,
            :backtrack_window,
            :cloudwatch_logs_export_configuration,
            :engine_version,
            :allow_major_version_upgrade,
            :db_instance_parameter_group_name,
            :domain,
            :domain_iam_role_name,
            :scaling_configuration,
            :deletion_protection,
            :enable_http_endpoint,
            :copy_tags_to_snapshot,
          ]
        }

        # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us.
        # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
        def initialize(**args)
          super
          @config["groomer"] = MU::Config.defaultGroomer unless @config["groomer"]
          @groomclass = MU::Groomer.loadGroomer(@config["groomer"])

          @mu_name ||=
            if @config and @config['engine'] and @config["engine"].match(/^sqlserver/)
              @deploy.getResourceName(@config["name"], max_length: 15)
            else
              @deploy.getResourceName(@config["name"], max_length: 63)
            end

          @mu_name.gsub(/(--|-$)/i, "").gsub(/(_)/, "-").gsub!(/^[^a-z]/i, "")
          if @config.has_key?("parameter_group_family")
            @config["parameter_group_name"] ||= @mu_name
          end

          if args[:from_cloud_desc] and args[:from_cloud_desc].is_a?(Aws::RDS::Types::DBCluster)
            @config['create_cluster'] = true
          end
          if @config['source']
            @config["source"] = MU::Config::Ref.get(@config["source"])
          elsif @config["read_replica_of"]
            @config["source"] = MU::Config::Ref.get(@config["read_replica_of"])
          end
        end

        # Called automatically by {MU::Deploy#createResources}
        # @return [String]: The cloud provider's identifier for this database instance.
        def create
          # RDS is picky, we can't just use our regular node names for things like
          # the default schema or username. And it varies from engine to engine.
          basename = @config["name"]+@deploy.timestamp+MU.seed.downcase
          basename.gsub!(/[^a-z0-9]/i, "")
          @config["db_name"] = MU::Cloud::AWS::Database.getName(basename, type: "dbname", config: @config)
          @config['master_user'] = MU::Cloud::AWS::Database.getName(basename, type: "dbuser", config: @config) unless @config['master_user']
          @cloud_id = @mu_name

          # Lets make sure automatic backups are enabled when DB instance is deployed in Multi-AZ so failover actually works. Maybe default to 1 instead?
          if @config['multi_az_on_create'] or @config['multi_az_on_deploy'] or @config["create_cluster"]
            if @config["backup_retention_period"].nil? or @config["backup_retention_period"] == 0
              @config["backup_retention_period"] = 35
              MU.log "Multi-AZ deployment specified but backup retention period disabled or set to 0. Changing to #{@config["backup_retention_period"]} ", MU::WARN
            end

            if @config["preferred_backup_window"].nil?
              @config["preferred_backup_window"] = "05:00-05:30"
              MU.log "Multi-AZ deployment specified but no backup window specified. Changing to #{@config["preferred_backup_window"]} ", MU::WARN
            end
          end

          @config["snapshot_id"] =
            if @config["creation_style"] == "existing_snapshot"
              getExistingSnapshot ? getExistingSnapshot : createNewSnapshot
            elsif @config["creation_style"] == "new_snapshot"
              createNewSnapshot
            end

          @config["subnet_group_name"] = @mu_name if @vpc

          if @config["create_cluster"]
            getPassword
            manageSubnetGroup

            if @config.has_key?("parameter_group_family")
              manageDbParameterGroup(true)
            end

            @config["cluster_identifier"] ||= @cloud_id

            if @config['creation_style'] == "point_in_time"
              create_point_in_time
            else
              create_basic
            end

            wait_until_available

            if %w{existing_snapshot new_snapshot point_in_time}.include?(@config["creation_style"])
              modify_db_cluster_struct = {
                db_cluster_identifier: @cloud_id,
                apply_immediately: true,
                backup_retention_period: @config["backup_retention_period"],
                db_cluster_parameter_group_name: @config["parameter_group_name"],
                master_user_password: @config["password"],
                preferred_backup_window: @config["preferred_backup_window"]
              }

              modify_db_cluster_struct[:preferred_maintenance_window] = @config["preferred_maintenance_window"] if @config["preferred_maintenance_window"]
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).modify_db_cluster(modify_db_cluster_struct)
              wait_until_available
            end

            do_naming
          elsif @config["add_cluster_node"]
            add_cluster_node
          else
            add_basic
          end
        end

        # Canonical Amazon Resource Number for this resource
        # @return [String]
        def arn
          cloud_desc.db_instance_arn
        end

        # Locate an existing Database or Databases and return an array containing matching AWS resource descriptors for those that match.
        # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching Databases
        def self.find(**args)
          found = {}

          if args[:cloud_id]
            if !args[:cluster]
              begin
                resp = MU::Cloud::AWS.rds(region: args[:region], credentials: args[:credentials]).describe_db_instances(db_instance_identifier: args[:cloud_id]).db_instances.first
                return { args[:cloud_id] => resp } if resp
              rescue Aws::RDS::Errors::DBInstanceNotFound
                MU.log "No results found looking for RDS instance #{args[:cloud_id]}", MU::DEBUG
              end
            end
            begin
              resp = MU::Cloud::AWS.rds(region: args[:region], credentials: args[:credentials]).describe_db_clusters(db_cluster_identifier: args[:cloud_id]).db_clusters.first
            rescue Aws::RDS::Errors::DBClusterNotFoundFault
              MU.log "No results found looking for RDS cluster #{args[:cloud_id]}", MU::DEBUG
            end
            return { args[:cloud_id] => resp } if resp

          else
            fetch = Proc.new { |noun|
              resp = MU::Cloud::AWS.rds(credentials: args[:credentials], region: args[:region]).send("describe_db_#{noun}s".to_sym)
              resp.send("db_#{noun}s").each { |db|
                found[db.send("db_#{noun}_identifier".to_sym)] = db
              }
            }
            if args[:cluster] or !args.has_key?(:cluster)
              fetch.call("cluster")
            end
            if !args[:cluster]
              fetch.call("instance")
            end
            if args[:tag_key] and args[:tag_value]
              keep = []
              found.each_pair { |id, desc|
                noun = desc.is_a?(Aws::RDS::Types::DBCluster) ? "cluster" : "db"
                resp = MU::Cloud::AWS.rds(credentials: args[:credentials], region: args[:region]).list_tags_for_resource(
                  resource_name: MU::Cloud::AWS::Database.getARN(id, noun, "rds", region: args[:region], credentials: args[:credentials])
                )
                if resp and resp.tag_list
                  resp.tag_list.each { |tag|
                    if tag.key == args[:tag_key] and tag.value == args[:tag_value]
                      keep << id
                      break
                    end
                  }
                end
              }
              found.reject! { |k, _v| !keep.include?(k) }
            end
          end

          return found
        end
        
        # Reverse-map our cloud description into a runnable config hash.
        # We assume that any values we have in +@config+ are placeholders, and
        # calculate our own accordingly based on what's live in the cloud.
        def toKitten(**_args)
          bok = {
            "cloud" => "AWS",
            "region" => @region,
            "credentials" => @credentials,
            "cloud_id" => @cloud_id,
          }

          # Don't adopt cluster members, they'll be picked up by the parent
          # cluster
          if !@config["create_cluster"] and cloud_desc.db_cluster_identifier and !cloud_desc.db_cluster_identifier.empty?
            return nil
          end

          noun = @config["create_cluster"] ? "cluster" : "db"
          tags = MU::Cloud::AWS.rds(credentials: @credentials, region: @region).list_tags_for_resource(
            resource_name: MU::Cloud::AWS::Database.getARN(@cloud_id, noun, "rds", region: @region, credentials: @credentials)
          ).tag_list
          if tags and !tags.empty?
            bok['tags'] = MU.structToHash(tags, stringify_keys: true)
            bok['name'] = MU::Adoption.tagsToName(bok['tags'])
          end
          bok["name"] ||= @cloud_id
          bok['engine'] = cloud_desc.engine
          bok['engine_version'] = cloud_desc.engine_version
          bok['master_user'] = cloud_desc.master_username
          bok['backup_retention_period'] = cloud_desc.backup_retention_period
          bok["create_cluster"] = true if @config['create_cluster']

          params = if bok['create_cluster']
            MU::Cloud::AWS.rds(credentials: @credentials, region: @region).describe_db_cluster_parameters(
              db_cluster_parameter_group_name: cloud_desc.db_cluster_parameter_group
            ).parameters
          else
            MU::Cloud::AWS.rds(credentials: @credentials, region: @region).describe_db_parameters(
              db_parameter_group_name: cloud_desc.db_parameter_groups.first.db_parameter_group_name
            ).parameters
          end

          params.reject! { |p| ["engine-default", "system"].include?(p.source) }
          if params and params.size > 0
            bok[(bok['create_cluster'] ? "cluster_" : "")+'parameter_group_parameters'] = params.map { |p|
              { "key" => p.parameter_name, "value" => p.parameter_value }
            }
          end

          bok['add_firewall_rules'] = cloud_desc.vpc_security_groups.map { |sg|
            MU::Config::Ref.get(
              id: sg.vpc_security_group_id,
              cloud: "AWS",
              credentials: @credentials,
              region: @region,
              type: "firewall_rules",
            )
          }
          bok['preferred_backup_window'] = cloud_desc.preferred_backup_window
          bok['preferred_maintenance_window'] = cloud_desc.preferred_maintenance_window
          bok['backup_retention_period'] = cloud_desc.backup_retention_period if cloud_desc.backup_retention_period > 1
          bok['multi_az_on_groom'] = true if cloud_desc.multi_az
          bok['storage_encrypted'] = true if cloud_desc.storage_encrypted

          if bok['create_cluster']
            bok['cluster_node_count'] = cloud_desc.db_cluster_members.size
            bok['cluster_mode'] = cloud_desc.engine_mode
            bok['port'] = cloud_desc.port

            sizes = []
            vpcs = []
            # we have no sensible way to handle heterogenous cluster members, so
            # for now just assume they're all the same
            cloud_desc.db_cluster_members.each { |db|
              member = MU::Cloud::AWS::Database.find(cloud_id: db.db_instance_identifier, region: @region, credentials: @credentials).values.first

              sizes << member.db_instance_class
              if member.db_subnet_group and member.db_subnet_group.vpc_id
                vpcs << member.db_subnet_group
              end
              bok
            }
            sizes.uniq!
            vpcs.uniq!
            bok['size'] = sizes.sort.first if !sizes.empty?
            if !vpcs.empty?
              myvpc = MU::MommaCat.findStray("AWS", "vpc", cloud_id: vpcs.sort.first.vpc_id, credentials: @credentials, region: @region, dummy_ok: true, no_deploy_search: true).first
              bok['vpc'] = myvpc.getReference(vpcs.sort.first.subnets.map { |s| s.subnet_identifier })
            end
          else
            bok['size'] = cloud_desc.db_instance_class
            bok['auto_minor_version_upgrade'] = true if cloud_desc.auto_minor_version_upgrade
            if cloud_desc.db_subnet_group
              myvpc = MU::MommaCat.findStray("AWS", "vpc", cloud_id: cloud_desc.db_subnet_group.vpc_id, credentials: @credentials, region: @region, dummy_ok: true, no_deploy_search: true).first
              bok['vpc'] = myvpc.getReference(cloud_desc.db_subnet_group.subnets.map { |s| s.subnet_identifier })
            end
            bok['storage_type'] = cloud_desc.storage_type
            bok['storage'] = cloud_desc.allocated_storage
            bok['license_model'] = cloud_desc.license_model
            bok['publicly_accessible'] = true if cloud_desc.publicly_accessible
            bok['port'] = cloud_desc.endpoint.port

            if cloud_desc.read_replica_source_db_instance_identifier
              bok['read_replica_of'] = MU::Config::Ref.get(
                id: cloud_desc.read_replica_source_db_instance_identifier.split(/:/).last,
                name: cloud_desc.read_replica_source_db_instance_identifier.split(/:/).last,
                cloud: "AWS",
                region: cloud_desc.read_replica_source_db_instance_identifier.split(/:/)[3],
                credentials: @credentials,
                type: "databases",
              )
            end
          end

          if cloud_desc.enabled_cloudwatch_logs_exports and
             cloud_desc.enabled_cloudwatch_logs_exports.size > 0
            bok['cloudwatch_logs'] = cloud_desc.enabled_cloudwatch_logs_exports
          end

          bok
        end

        # Construct an Amazon Resource Name for an RDS resource. The RDS API is
        # peculiar, and we often need this identifier in order to do things that
        # the other APIs can do with shorthand.
        # @param resource [String]: The name of the resource
        # @param resource_type [String]: The type of the resource (one of `db, es, og, pg, ri, secgrp, snapshot, subgrp`)
        # @param client_type [String]: The name of the client (eg. elasticache, rds, ec2, s3)
        # @param region [String]: The region in which the resource resides.
        # @param account_number [String]: The account in which the resource resides.
        # @return [String]
        def self.getARN(resource, resource_type, client_type, region: MU.curRegion, account_number: nil, credentials: nil)
          account_number ||= MU::Cloud::AWS.credToAcct(credentials)
          aws_str = MU::Cloud::AWS.isGovCloud?(region) ? "aws-us-gov" : "aws"
          "arn:#{aws_str}:#{client_type}:#{region}:#{account_number}:#{resource_type}:#{resource}"
        end

        # Construct all our tags.
        # @return [Array]: All our standard tags and any custom tags.
        def allTags
          @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
        end

        # Create a subnet group for a database.
        def manageSubnetGroup
          # Finding subnets, creating security groups/adding holes, create subnet group
          subnet_ids = []

dependencies
          raise MuError.new "Didn't find the VPC specified for #{@mu_name}", details: @config["vpc"].to_h unless @vpc

          mySubnets.each { |subnet|
            next if @config["publicly_accessible"] and subnet.private?
            subnet_ids << subnet.cloud_id
          }

          if @config['creation_style'] == "existing"
            srcdb_vpc = @config['source'].kitten.cloud_desc.db_subnet_group.vpc_id
            if srcdb_vpc != @vpc.cloud_id
              MU.log "#{self} is deploying into #{@vpc.cloud_id}, but our source database, #{@config['identifier']}, is in #{srcdb_vpc}", MU::ERR
              raise MuError, "Can't use 'existing' to deploy into a different VPC from the source database; try 'new_snapshot' instead"
            end
          end

          if subnet_ids.empty?
            raise MuError, "Couldn't find subnets in #{@vpc} to add to #{@config["subnet_group_name"]}. Make sure the subnets are valid and publicly_accessible is set correctly"
          else
            resp = begin
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).describe_db_subnet_groups(
                db_subnet_group_name: @config["subnet_group_name"]
              )
# XXX ensure subnet group matches our config?
            rescue ::Aws::RDS::Errors::DBSubnetGroupNotFoundFault
              # Create subnet group
              resp = MU::Cloud::AWS.rds(region: @region, credentials: @credentials).create_db_subnet_group(
                db_subnet_group_name: @config["subnet_group_name"],
                db_subnet_group_description: @config["subnet_group_name"],
                subnet_ids: subnet_ids,
                tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
              )
              # The API forces it to lowercase, for some reason? Maybe not
              # always? Just rely on what it says.
              @config["subnet_group_name"] = resp.db_subnet_group.db_subnet_group_name
              resp
            end

            myFirewallRules.each { |sg|
              next if sg.cloud_desc.vpc_id != @vpc.cloud_id
              @config["vpc_security_group_ids"] ||= []
              @config["vpc_security_group_ids"] << sg.cloud_id
            }
          end

          allowBastionAccess
        end

        # Create a database parameter group.
        def manageDbParameterGroup(cluster = false, create: true)
          return if !@config["parameter_group_name"]
          name_param = cluster ? :db_cluster_parameter_group_name : :db_parameter_group_name
          fieldname = cluster ? "cluster_parameter_group_parameters" : "db_parameter_group_parameters"

          params = {
            db_parameter_group_family: @config["parameter_group_family"],
            description: "Parameter group for #{@mu_name}",
            tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
          }
          params[name_param] = @config["parameter_group_name"]

          if create
            MU.log "Creating a #{cluster ? "cluster" : "database" } parameter group #{@config["parameter_group_name"]}"

            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).send(cluster ? :create_db_cluster_parameter_group : :create_db_parameter_group, params)
          end


          if @config[fieldname] and !@config[fieldname].empty?

            old_values = MU::Cloud::AWS.rds(credentials: @credentials, region: @region).send(cluster ? :describe_db_cluster_parameters : :describe_db_parameters, { name_param => @config["parameter_group_name"] } ).parameters
            old_values.map! { |p| [p.parameter_name, p.parameter_value] }.flatten
            old_values = old_values.to_h

            params = []
            @config[fieldname].each { |item|
              next if old_values[item["name"]] == item['value']
              params << {parameter_name: item['name'], parameter_value: item['value'], apply_method: item['apply_method']}
            }
            return if params.empty?

            MU.log "Modifying parameter group #{@config["parameter_group_name"]}", MU::NOTICE, details: params.map { |p| { p[:parameter_name] => p[:parameter_value] } }

            MU.retrier([Aws::RDS::Errors::InvalidDBParameterGroupState], wait: 30, max: 10) {
              if cluster
                MU::Cloud::AWS.rds(region: @region, credentials: @credentials).modify_db_cluster_parameter_group(
                  db_cluster_parameter_group_name: @config["parameter_group_name"],
                  parameters: params
                )
              else
                MU::Cloud::AWS.rds(region: @region, credentials: @credentials).modify_db_parameter_group(
                  db_parameter_group_name: @config["parameter_group_name"],
                  parameters: params
                )
              end
            }
          end
        end

        # Called automatically by {MU::Deploy#createResources}
        def groom
          cloud_desc(use_cache: false)
          manageSubnetGroup if @vpc
          manageDbParameterGroup(@config["create_cluster"], create: false)

          noun = @config['create_cluster'] ? "cluster" : "instance"

          mods = {
            "db_#{noun}_identifier".to_sym => @cloud_id
          }

          basicParams.each_pair { |k, v|
            next if v.nil? or !MODIFIABLE[noun].include?(k)
            if cloud_desc.respond_to?(k) and cloud_desc.send(k) != v
              mods[k] = v
            end
          }

          existing_sgs = cloud_desc.vpc_security_groups.map { |sg|
            sg.vpc_security_group_id
          }.sort

          if !@config["add_cluster_node"] and !@config["member_of_cluster"] and
             @config["vpc_security_group_ids"] and
             existing_sgs != @config["vpc_security_group_ids"].sort
            mods[:vpc_security_group_ids] = @config["vpc_security_group_ids"]
          end


          if @config['cloudwatch_logs'] and cloud_desc.enabled_cloudwatch_logs_exports.sort != @config['cloudwatch_logs'].sort
            mods[:cloudwatch_logs_export_configuration] = {
              enable_log_types: @config['cloudwatch_logs'],
              disable_log_types: cloud_desc.enabled_cloudwatch_logs_exports - @config['cloudwatch_logs']
            }
          end

          if @config["create_cluster"]
            @config['cluster_node_count'] ||= 1
            if @config['cluster_mode'] == "serverless"
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).modify_current_db_cluster_capacity(
                db_cluster_identifier: @cloud_id,
                capacity: @config['cluster_node_count']
              )
            end
          else
            # Run SQL on deploy
            if @config['run_sql_on_deploy']
              run_sql_commands
            end

            if !cloud_desc.multi_az and (@config['multi_az_on_deploy'] or @config['multi_az_on_create'])
              mods[:multi_az] = true
            end

# XXX how do we guard this? do we?
#              master_user_password: @config["password"],
#            end

# XXX it's a stupid array
#              db_parameter_group_name: @config["parameter_group_name"],
          end

          if mods.size > 1
            MU.log "Modifying RDS instance #{@cloud_id}", MU::NOTICE, details: mods
            mods[:apply_immediately] = true
            mods[:allow_major_version_upgrade] = true
            wait_until_available
            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).send("modify_db_#{noun}".to_sym, mods)
            wait_until_available
          end

        end

        # Generate database user, database identifier, database name based on engine-specific constraints
        # @return [String]: Name
        def self.getName(basename, type: 'dbname', config: nil)
          if type == 'dbname'
            # Apply engine-specific db name constraints
            if config["engine"] =~ /^oracle/
              (MU.seed.downcase+config["name"])[0..7]
            elsif config["engine"] =~ /^sqlserver/
              nil
            elsif config["engine"] =~ /^mysql/
              basename[0..63]
            elsif config["engine"] =~ /^aurora/
              (MU.seed.downcase+config["name"])[0..7]
            else
              basename
            end
          elsif type == 'dbuser'
            # Apply engine-specific master username constraints
            if config["engine"] =~ /^oracle/
              basename[0..29].gsub(/[^a-z0-9]/i, "")
            elsif config["engine"] =~ /^sqlserver/
              basename[0..127].gsub(/[^a-z0-9]/i, "")
            elsif config["engine"] =~ /^(mysql|maria)/
              basename[0..15].gsub(/[^a-z0-9]/i, "")
            elsif config["engine"] =~ /^aurora/
              basename[0..15].gsub(/[^a-z0-9]/i, "")
            else
              basename.gsub(/[^a-z0-9]/i, "")
            end
          end
        end

        # Permit a host to connect to the given database instance.
        # @param cidr [String]: The CIDR-formatted IP address or block to allow access.
        # @return [void]
        def allowHost(cidr)
          # If we're an old, Classic-style database with RDS-specific
          # authorization, punch holes in that.
          if !cloud_desc.db_security_groups.empty?
            cloud_desc.db_security_groups.each { |rds_sg|
              begin
                MU::Cloud::AWS.rds(region: @region, credentials: @credentials).authorize_db_security_group_ingress(
                    db_security_group_name: rds_sg.db_security_group_name,
                    cidrip: cidr
                )
              rescue Aws::RDS::Errors::AuthorizationAlreadyExists
                MU.log "CIDR #{cidr} already in database instance #{@cloud_id} security group", MU::WARN
              end
            }
          end

          # Otherwise go get our generic EC2 ruleset and punch a hole in it
          myFirewallRules.each { |sg|
            sg.addRule([cidr], proto: "tcp", port: cloud_desc.endpoint.port)
            break
          }
        end

        # Return the metadata for this ContainerCluster
        # @return [Hash]
        def notify
          deploy_struct = MU.structToHash(cloud_desc, stringify_keys: true)
          deploy_struct['cloud_id'] = @cloud_id
          deploy_struct["region"] ||= @region
          deploy_struct["db_name"] ||= @config['db_name']
          deploy_struct
        end

        # Generate a snapshot from the database described in this instance.
        # @return [String]: The cloud provider's identifier for the snapshot.
        def createNewSnapshot
          snap_id = @deploy.getResourceName(@config["name"]) + Time.new.strftime("%M%S").to_s
          src_ref = MU::Config::Ref.get(@config["source"])
          src_ref.kitten(@deploy)
          if !src_ref.id
            raise MuError.new "#{@mu_name} failed to get an id from reference for creating a snapshot", details: @config['source']
          end
          params = {
            :tags => @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
          }
          if @config["create_cluster"]
            params[:db_cluster_snapshot_identifier] = snap_id
            params[:db_cluster_identifier] = src_ref.id
          else
            params[:db_snapshot_identifier] = snap_id
            params[:db_instance_identifier] = src_ref.id
          end

          MU.retrier([Aws::RDS::Errors::InvalidDBInstanceState, Aws::RDS::Errors::InvalidDBClusterStateFault], wait: 60, max: 10) {
            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).send("create_db_#{@config['create_cluster'] ? "cluster_" : ""}snapshot".to_sym, params)
          }

          loop_if = Proc.new {
            if @config["create_cluster"]
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).describe_db_cluster_snapshots(db_cluster_snapshot_identifier: snap_id).db_cluster_snapshots.first.status != "available"
            else
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).describe_db_snapshots(db_snapshot_identifier: snap_id).db_snapshots.first.status != "available"
            end
          }

          MU.retrier(wait: 15, loop_if: loop_if) { |retries, _wait|
            MU.log "Waiting for RDS snapshot of #{src_ref.id} to be ready...", MU::NOTICE if retries % 20 == 0
          }

          return snap_id
        end

        # Fetch the latest snapshot of the database described in this instance.
        # @return [String]: The cloud provider's identifier for the snapshot.
        def getExistingSnapshot
          src_ref = MU::Config::Ref.get(@config["source"])
          resp =
            if @config["create_cluster"]
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).describe_db_cluster_snapshots(db_cluster_snapshot_identifier: src_ref.id)
            else
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).describe_db_snapshots(db_snapshot_identifier: src_ref.id)
            end

          snapshots = @config["create_cluster"] ? resp.db_cluster_snapshots : resp.db_snapshots

          if snapshots.empty?
            nil
          else
            sorted_snapshots = snapshots.sort_by { |snap| snap.snapshot_create_time }
            @config["create_cluster"] ? sorted_snapshots.last.db_cluster_snapshot_identifier : sorted_snapshots.last.db_snapshot_identifier
          end
        end

        # Does this resource type exist as a global (cloud-wide) artifact, or
        # is it localized to a region/zone?
        # @return [Boolean]
        def self.isGlobal?
          false
        end

        # Denote whether this resource implementation is experiment, ready for
        # testing, or ready for production use.
        def self.quality
          MU::Cloud::RELEASE
        end

        # @return [Array<Thread>]
        def self.threaded_resource_purge(describe_method, list_method, id_method, arn_type, region, credentials, ignoremaster, known: [], deploy_id: MU.deploy_id)
          deletia = []

          resp = MU::Cloud::AWS.rds(credentials: credentials, region: region).send(describe_method)
          resp.send(list_method).each { |resource|
            begin
              arn = MU::Cloud::AWS::Database.getARN(resource.send(id_method), arn_type, "rds", region: region, credentials: credentials)
              tags = MU::Cloud::AWS.rds(credentials: credentials, region: region).list_tags_for_resource(resource_name: arn).tag_list
            rescue Aws::RDS::Errors::InvalidParameterValue
              MU.log "Failed to fetch ARN of type #{arn_type} or tags of resource via #{id_method}", MU::WARN, details: [resource, arn]
              next
            end

            if should_delete?(tags, resource.send(id_method), ignoremaster, deploy_id, MU.mu_public_ip, known)
              deletia << resource.send(id_method)
            end
          }

          threads = []
          deletia.each { |id|
            threads << Thread.new(id) { |resource_id|
              yield(resource_id)
            }
          }

          threads
        end

        # Called by {MU::Cleanup}. Locates resources that were created by the
        # currently-loaded deployment, and purges them.
        # @param noop [Boolean]: If true, will only print what would be done
        # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
        # @param region [String]: The cloud provider region in which to operate
        # @return [void]
        def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, region: MU.curRegion, flags: {})

          threads = []

          ["instance", "cluster"].each { |type|
            threads.concat threaded_resource_purge("describe_db_#{type}s".to_sym, "db_#{type}s".to_sym, "db_#{type}_identifier".to_sym, (type == "instance" ? "db" : "cluster"), region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
              terminate_rds_instance(nil, noop: noop, skipsnapshots: flags["skipsnapshots"], region: region, deploy_id: deploy_id, cloud_id: id, mu_name: id.upcase, credentials: credentials, cluster: (type == "cluster"), known: flags['known'])
  
            }
          }
          threads.each { |t|
            t.join
          }

          threads = threaded_resource_purge(:describe_db_subnet_groups, :db_subnet_groups, :db_subnet_group_name, "subgrp", region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
            MU.log "Deleting RDS subnet group #{id}"
            MU.retrier([Aws::RDS::Errors::InvalidDBSubnetGroupStateFault], wait: 30, max: 5, ignoreme: [Aws::RDS::Errors::DBSubnetGroupNotFoundFault]) {
              MU::Cloud::AWS.rds(region: region, credentials: credentials).delete_db_subnet_group(db_subnet_group_name: id) if !noop
            }
          }

          ["db", "db_cluster"].each { |type|
            threads.concat threaded_resource_purge("describe_#{type}_parameter_groups".to_sym, "#{type}_parameter_groups".to_sym, "#{type}_parameter_group_name".to_sym, (type == "db" ? "pg" : "cluster-pg"), region, credentials, ignoremaster, known: flags['known'], deploy_id: deploy_id) { |id|
              MU.log "Deleting RDS #{type} parameter group #{id}"
              MU.retrier([Aws::RDS::Errors::InvalidDBParameterGroupState], wait: 30, max: 5, ignoreme: [Aws::RDS::Errors::DBParameterGroupNotFound]) {
                MU::Cloud::AWS.rds(region: region, credentials: credentials).send("delete_#{type}_parameter_group", { "#{type}_parameter_group_name".to_sym => id }) if !noop
              }
            }
          }

          # Wait for all of the databases subnet/parameter groups to finish cleanup before proceeding
          threads.each { |t|
            t.join
          }
        end

        # Cloud-specific configuration properties.
        # @param _config [MU::Config]: The calling MU::Config object
        # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
        def self.schema(_config)
          toplevel_required = []
          rds_parameters_primitive = {
            "type" => "array",
            "minItems" => 1,
            "items" => {
              "description" => "The database parameter group parameter to change and when to apply the change.",
              "type" => "object",
              "title" => "Database Parameter",
              "required" => ["name", "value"],
              "additionalProperties" => false,
              "properties" => {
                "name" => {
                  "type" => "string"
                },
                "value" => {
                  "type" => "string"
                },
                "apply_method" => {
                  "enum" => ["pending-reboot", "immediate"],
                  "default" => "immediate",
                  "type" => "string"
                }
              }
            }
          }


          schema = {
            "db_parameter_group_parameters" => rds_parameters_primitive,
            "cluster_parameter_group_parameters" => rds_parameters_primitive,
            "parameter_group_family" => {
              "type" => "String",
              "description" => "An RDS parameter group family. See also https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html"
            },
            "cluster_mode" => {
              "type" => "string",
              "description" => "The DB engine mode of the DB cluster",
              "enum" => ["provisioned", "serverless", "parallelquery", "global", "multimaster"],
              "default" => "provisioned"
            },
            "storage_type" => {
              "enum" => ["standard", "gp2", "io1"],
              "type" => "string",
              "default" => "gp2"
            },
            "cloudwatch_logs" => {
              "type" => "array",
              "items" => {
                "type" => "string",
                "enum" => ["audit", "error", "general", "slowquery", "profiler", "postgresql", "alert", "listener", "trace", "upgrade", "agent"]
              }
            },
            "serverless_scaling" => {
              "type" => "object",
              "description" => "Scaling configuration for a +serverless+ Aurora cluster",
              "default" => {
                "auto_pause" => false,
                "min_capacity" => 2,
                "max_capacity" => 2
              },
              "properties" => {
                "auto_pause" => {
                  "type" => "boolean",
                  "description" => "A value that specifies whether to allow or disallow automatic pause for an Aurora DB cluster in serverless DB engine mode",
                  "default" => false
                },
                "min_capacity" => {
                  "type" => "integer",
                  "description" => "The minimum capacity for an Aurora DB cluster in serverless DB engine mode.",
                  "default" => 2,
                  "enum" => [2, 4, 8, 16, 32, 64, 128, 256]
                },
                "max_capacity" => {
                  "type" => "integer",
                  "description" => "The maximum capacity for an Aurora DB cluster in serverless DB engine mode.",
                  "default" => 2,
                  "enum" => [2, 4, 8, 16, 32, 64, 128, 256]
                },
                "seconds_until_auto_pause" => {
                  "type" => "integer",
                  "description" => "A DB cluster can be paused only when it's idle (it has no connections). If a DB cluster is paused for more than seven days, the DB cluster might be backed up with a snapshot. In this case, the DB cluster is restored when there is a request to connect to it.",
                  "default" => 86400
                }
              }
            },
            "license_model" => {
              "type" => "string",
              "enum" => ["license-included", "bring-your-own-license", "general-public-license", "postgresql-license"]
            },
            "ingress_rules" => MU::Cloud.resourceClass("AWS", "FirewallRule").ingressRuleAddtlSchema
          }
          [toplevel_required, schema]
        end

        @@engine_cache= {}
        def self.get_supported_engines(region = MU.myRegion, credentials = nil, engine: nil)
          @@engine_cache ||= {}
          @@engine_cache[credentials] ||= {}
          @@engine_cache[credentials][region] ||= {}

          if !@@engine_cache[credentials][region].empty?
            return engine ? @@engine_cache[credentials][region][engine] : @@engine_cache[credentials][region]
          end

          engines = {}

          resp = MU::Cloud::AWS.rds(credentials: credentials, region: region).describe_db_engine_versions

          if resp and resp.db_engine_versions
            resp.db_engine_versions.each { |version|
              engines[version.engine] ||= {
                "versions" => [],
                "families" => [],
                "features" => {},
                "raw" => {}
              }
              engines[version.engine]['versions'] << version.engine_version
              engines[version.engine]['families'] << version.db_parameter_group_family
              engines[version.engine]['raw'][version.engine_version] = version
              [:supports_read_replica, :supports_log_exports_to_cloudwatch_logs].each { |feature|
                if version.respond_to?(feature) and version.send(feature) == true
                  engines[version.engine]['features'][version.engine_version] ||= []
                  engines[version.engine]['features'][version.engine_version] << feature
                end
              }

            }
            engines.each_key { |e|
              engines[e]["versions"].uniq!
              engines[e]["versions"].sort! { |a, b| MU.version_sort(a, b) }
              engines[e]["families"].uniq!
            }

          else
            MU.log "Failed to get list of valid RDS engine versions in #{db['region']}, proceeding without proper validation", MU::WARN
          end

          @@engine_cache[credentials][region] = engines
          return engine ? @@engine_cache[credentials][region][engine] : @@engine_cache[credentials][region]
        end
        private_class_method :get_supported_engines

        # Make sure any source database/cluster/snapshot we've asked for exists
        # and is valid.
        def self.validate_source_data(db)
          ok = true

          if db['creation_style'] == "existing_snapshot" and
             !db['create_cluster'] and
             db['source'] and db["source"]["id"] and db['source']["id"].match(/:cluster-snapshot:/)
            MU.log "Database #{db['name']}: Existing snapshot #{db["source"]["id"]} looks like a cluster snapshot, but create_cluster is not set. Add 'create_cluster: true' if you're building an RDS cluster.", MU::ERR
            ok = false
          elsif db["creation_style"] == "existing" or db["creation_style"] == "new_snapshot"
            begin
              MU::Cloud::AWS.rds(region: db['region']).describe_db_instances(
                db_instance_identifier: db['source']['id']
              )
            rescue Aws::RDS::Errors::DBInstanceNotFound
              MU.log "Source database was specified for #{db['name']}, but no such database exists in #{db['region']}", MU::ERR, db['source']
              ok = false
            end
          end

          ok
        end
        private_class_method :validate_source_data

        def self.validate_master_password(db)
          maxlen = case db['engine']
            when "mariadb", "mysql"
              41
            when "postgresql"
              41
            when /oracle/
              30
            when /sqlserver/
              128
            else
              return true
          end

          pw = if !db['password'].nil?
            db['password']
          elsif db['auth_vault'] and !db['auth_vault'].empty?
            groomclass = MU::Groomer.loadGroomer(db['groomer'])
            pw = groomclass.getSecret(
              vault: db['auth_vault']['vault'],
              item: db['auth_vault']['item'],
              field: db['auth_vault']['password_field']
            )
            return true if pw.nil?
            pw
          end

          if pw and (pw.length < 8 or pw.match(/[\/\\@\s]/) or pw.length > maxlen)
            MU.log "Database password specified in 'password' or 'auth_vault' doesn't meet RDS requirements. Must be between 8 and #{maxlen} chars and have only ASCII characters other than /, @, \", or [space].", MU::ERR
            return false
          end

          true
        end
        private_class_method :validate_master_password

        # Cloud-specific pre-processing of {MU::Config::BasketofKittens::databases}, bare and unvalidated.
        # @param db [Hash]: The resource to process and validate
        # @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a ember
        # @return [Boolean]: True if validation succeeded, False otherwise
        def self.validateConfig(db, _configurator)
          ok = true

          ok = false if !validate_source_data(db)

          ok = false if !validate_engine(db)

          ok = false if !valid_read_replica?(db)

          ok = false if !valid_cloudwatch_logs?(db)

          db["license_model"] ||=
            if ["postgres", "postgresql", "aurora-postgresql"].include?(db["engine"])
              "postgresql-license"
            elsif ["mysql", "mariadb"].include?(db["engine"])
              "general-public-license"
            else
              "license-included"
            end

          ok = false if !validate_master_password(db)

          if db["multi_az_on_create"] and db["multi_az_on_deploy"]
            MU.log "Both of multi_az_on_create and multi_az_on_deploy cannot be true", MU::ERR
            ok = false
          end

          if (db["db_parameter_group_parameters"] or db["cluster_parameter_group_parameters"]) and db["parameter_group_family"].nil?
            engine = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])
            db["parameter_group_family"] = engine['raw'][db['engine_version']].db_parameter_group_family
          end

          # Adding rules for Database instance storage. This varies depending on storage type and database type. 
          if !db["storage"].nil? and !db["create_cluster"] and !db["add_cluster_node"] and !STORAGE_RANGES[db["storage_type"]][db['engine']].include?(db["storage"])
            MU.log "Database storage size is set to #{db["storage"]}. #{db["engine"]} only supports storage sizes from #{STORAGE_RANGES[db["storage_type"]][db['engine']]} GB for #{db["storage_type"]} volumes.", MU::ERR
            ok = false
          end

          ok = false if !validate_network_cfg(db)

          ok
        end

        private

        def genericParams
          params = if @config['create_cluster']
            paramhash = {
              db_cluster_identifier: @cloud_id,
              engine: @config["engine"],
              vpc_security_group_ids: @config["vpc_security_group_ids"],
              tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
            }

            if @vpc and @config["subnet_group_name"]
              paramhash[:db_subnet_group_name] = @config["subnet_group_name"]
            end

            if @config['cloudwatch_logs']
              paramhash[:enable_cloudwatch_logs_exports ] = @config['cloudwatch_logs']
            end
            if @config['cluster_mode']
              paramhash[:engine_mode] = @config['cluster_mode']
              if @config['cluster_mode'] == "serverless"
                paramhash[:scaling_configuration] = {
                  :auto_pause => @config['serverless_scaling']['auto_pause'],
                  :min_capacity => @config['serverless_scaling']['min_capacity'],
                  :max_capacity => @config['serverless_scaling']['max_capacity'],
                  :seconds_until_auto_pause => @config['serverless_scaling']['seconds_until_auto_pause']
                }
              end
            end
            paramhash
          else
            {
              db_instance_identifier: @cloud_id,
              db_instance_class: @config["size"],
              engine: @config["engine"],
              auto_minor_version_upgrade: @config["auto_minor_version_upgrade"],
              license_model: @config["license_model"],
              db_subnet_group_name: @config["subnet_group_name"],
              vpc_security_group_ids: @config["vpc_security_group_ids"],
              publicly_accessible: @config["publicly_accessible"],
              copy_tags_to_snapshot: true,
              tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } }
            }
          end

          if %w{existing_snapshot new_snapshot}.include?(@config["creation_style"])
            if @config['create_cluster']
              params[:snapshot_identifier] = @config["snapshot_id"]
            else
              params[:db_snapshot_identifier] = @config["snapshot_id"]
            end
          end

          params
        end


        def self.validate_network_cfg(db)
          ok = true

          if !db['vpc']
            db["vpc"] = MU::Cloud.resourceClass("AWS", "VPC").defaultVpc(db['region'], db['credentials'])
            if db['vpc'] and !(db['engine'].match(/sqlserver/) and db['create_read_replica'])
              MU.log "Using default VPC for database '#{db['name']}; this sets 'publicly_accessible' to true.", MU::WARN
              db['publicly_accessible'] = true
            end
          else
            if db["vpc"]["subnet_pref"] == "all_public" and !db['publicly_accessible'] and (db["vpc"]['subnets'].nil? or db["vpc"]['subnets'].empty?)
              MU.log "Setting publicly_accessible to true on database '#{db['name']}', since deploying into public subnets.", MU::WARN
              db['publicly_accessible'] = true
            elsif db["vpc"]["subnet_pref"] == "all_private" and db['publicly_accessible']
              MU.log "Setting publicly_accessible to false on database '#{db['name']}', since deploying into private subnets.", MU::NOTICE
              db['publicly_accessible'] = false
            end
            if db['engine'].match(/sqlserver/) and db['create_read_replica']
              MU.log "SQL Server does not support read replicas in VPC deployments", MU::ERR
              ok = false
            end
          end

          ok
        end
        private_class_method :validate_network_cfg

        def self.valid_read_replica?(db)
          if !db['create_read_replica'] and !db['read_replica_of']
            return true
          end

          engine = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])
          if engine.nil? or !engine['features'] or !engine['features'][db['engine_version']]
            return true # we can't be sure, so let the API sort it out later
          end

          if !engine['features'][db['engine_version']].include?(:supports_read_replica)
            MU.log "Engine #{db['engine']} #{db['engine_version']} does not appear to support read replicas", MU::ERR
            return false
          end
          true
        end
        private_class_method :valid_read_replica?

        def self.valid_cloudwatch_logs?(db)
          return true if !db['cloudwatch_logs']
          engine = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])
          if engine.nil? or !engine['features'] or !engine['features'][db['engine_version']] or !engine['features'][db['engine_version']].include?(:supports_read_replica)
            MU.log "CloudWatch Logs not supported for #{db['engine']} #{db['engine_version']}", MU::ERR
            return false
          end

          ok = true
          db['cloudwatch_logs'].each { |logtype|
            if !engine['raw'][db['engine_version']].exportable_log_types.include?(logtype)
              ok = false
              MU.log "CloudWatch Log type #{logtype} is not valid for #{db['engine']} #{db['engine_version']}. List of valid types:", MU::ERR, details: engine['raw'][db['engine_version']].exportable_log_types
            end
          }

          ok
        end
        private_class_method :valid_cloudwatch_logs?

        def self.validate_engine(db)
          ok = true

          if db['create_cluster'] or db["member_of_cluster"] or db["add_cluster_node"] or (db['engine'] and db['engine'].match(/aurora/))
            case db['engine']
            when "mysql", "aurora", "aurora-mysql"
              if (db['engine_version'] and db["engine_version"].match(/^5\.6/)) or db["cluster_mode"] == "serverless"
                db["engine"] = "aurora"
                db["engine_version"] = "5.6"
                db['publicly_accessible'] = false
              else
                db["engine"] = "aurora-mysql"
              end
            when /postgres/
              db["engine"] = "aurora-postgresql"
            else
              ok = false
              MU.log "#{db['engine']} is not supported for clustering", MU::ERR
            end
            db["create_cluster"] = true if !(db["member_of_cluster"] or db["add_cluster_node"])
          end

          db["engine"] = "oracle-se2" if db["engine"] == "oracle"
          db["engine"] = "sqlserver-ex" if db["engine"] == "sqlserver"

          engine_cfg = get_supported_engines(db['region'], db['credentials'], engine: db['engine'])

          if !engine_cfg or engine_cfg['versions'].empty? or engine_cfg['families'].empty?
            MU.log "RDS engine #{db['engine']} reports no supported versions in #{db['region']}", MU::ERR, details: engine_cfg
            return false
          end

          # Resolve or default our engine version to something reasonable
          db['engine_version'] ||= engine_cfg['versions'].last
          if !engine_cfg['versions'].include?(db["engine_version"])
            db['engine_version'] = engine_cfg['versions'].grep(/^#{Regexp.quote(db["engine_version"])}/).last
          end
          if !engine_cfg['versions'].include?(db["engine_version"])
            MU.log "RDS engine '#{db['engine']}' version '#{db['engine_version']}' is not supported in #{db['region']}", MU::ERR, details: { "Known-good versions:" => engine_cfg['versions'].uniq.sort }
            ok = false
          end

          if db["parameter_group_family"] and
             !engine_cfg['families'].include?(db['parameter_group_family'])
            MU.log "RDS engine '#{db['engine']}' parameter group family '#{db['parameter_group_family']}' is not supported.", MU::ERR, details: engine_cfg['families'].uniq.sort
            ok = false
          end

          ok
        end
        private_class_method :validate_engine

        def add_basic

          getPassword
          if @config['source'].nil? or @region != @config['source'].region
            manageSubnetGroup if @vpc
          else
            MU.log "Note: Read Replicas automatically reside in the same subnet group as the source database, if they're both in the same region. This replica may not land in the VPC you intended.", MU::WARN
          end

          if @config.has_key?("parameter_group_family")
            manageDbParameterGroup
          end

          createDb
        end


        def add_cluster_node
          cluster = MU::Config::Ref.get(@config["member_of_cluster"]).kitten(@deploy)
          if cluster.nil? or cluster.cloud_id.nil?
            raise MuError.new "Failed to resolve parent cluster of #{@mu_name}", details: @config["member_of_cluster"].to_h
          end

          @config['cluster_identifier'] = cluster.cloud_id.downcase

          # We're overriding @config["subnet_group_name"] because we need each cluster member to use the cluster's subnet group instead of a unique subnet group
          @config["subnet_group_name"] = cluster.cloud_desc.db_subnet_group if @vpc
          @config["creation_style"] = "new" if @config["creation_style"] != "new"
          if @config.has_key?("parameter_group_family")
            manageDbParameterGroup
          end

          createDb
        end

        def basicParams
          params = genericParams
          params[:storage_encrypted] = @config["storage_encrypted"]
          params[:master_user_password] = @config['password']
          params[:engine_version] = @config["engine_version"]
          params[:vpc_security_group_ids] = @config["vpc_security_group_ids"]
          params[:preferred_maintenance_window] = @config["preferred_maintenance_window"] if @config["preferred_maintenance_window"]
          params[:backup_retention_period] = @config["backup_retention_period"] if @config["backup_retention_period"]

          if @config['create_cluster']
            params[:database_name] = @config["db_name"]
            params[:db_cluster_parameter_group_name] = @config["parameter_group_name"] if @config["parameter_group_name"]
          else
            params[:enable_cloudwatch_logs_exports] = @config['cloudwatch_logs'] if @config['cloudwatch_logs'] and !@config['cloudwatch_logs'].empty?
            params[:db_name] = @config["db_name"] if !@config['add_cluster_node']
            params[:db_parameter_group_name] = @config["parameter_group_name"] if @config["parameter_group_name"]
          end

          if @config['create_cluster'] or @config['add_cluster_node']
            params[:db_cluster_identifier] = @config["cluster_identifier"]
          else
            params[:storage_type] = @config["storage_type"] 
            params[:allocated_storage] = @config["storage"]
            params[:multi_az] = @config['multi_az_on_create']
          end

          noun = @config['create_cluster'] ? "cluster" : "instance"

          if noun == "cluster" or !params[:db_cluster_identifier]
            params[:backup_retention_period] = @config["backup_retention_period"]
            params[:preferred_backup_window] = @config["preferred_backup_window"]
            params[:master_username] = @config['master_user']
            params[:port] = @config["port"] if @config["port"]
            params[:iops] = @config["iops"] if @config['storage_type'] == "io1"
          end

          params
        end

        # creation_style = new, existing, new_snapshot, existing_snapshot
        def create_basic
          params = basicParams

          clean_parent_opts = Proc.new {
            [:storage_encrypted, :master_user_password, :engine_version, :allocated_storage, :backup_retention_period, :preferred_backup_window, :master_username, :db_name, :database_name].each { |p| params.delete(p) }
          }

          noun = @config["create_cluster"] ? "cluster" : "instance"

          MU.retrier([Aws::RDS::Errors::InvalidParameterValue, Aws::RDS::Errors::DBSubnetGroupNotFoundFault], max: 10, wait: 15) {
            if %w{existing_snapshot new_snapshot}.include?(@config["creation_style"])
              clean_parent_opts.call
              MU.log "Creating database #{noun} #{@cloud_id} from snapshot #{@config["snapshot_id"]}"
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).send("restore_db_#{noun}_from_#{noun == "instance" ? "db_" : ""}snapshot".to_sym, params)
            else
              clean_parent_opts.call if noun == "instance" and params[:db_cluster_identifier]
              MU.log "Creating pristine database #{noun} #{@cloud_id} (#{@config['name']}) in #{@region}", MU::NOTICE, details: params
              MU::Cloud::AWS.rds(region: @region, credentials: @credentials).send("create_db_#{noun}".to_sym, params)
            end
          }
        end

        # creation_style = point_in_time
        def create_point_in_time
          @config["source"].kitten(@deploy)
          if !@config["source"].id
            raise MuError.new "Database '#{@config['name']}' couldn't resolve cloud id for source database", details: @config["source"].to_h
          end

          params = genericParams
          params.delete(:db_instance_identifier)
          if @config['create_cluster']
            params[:source_db_cluster_identifier] = @config["source"].id
            params[:restore_to_time] = @config["restore_time"] unless @config["restore_time"] == "latest"
          else
            params[:source_db_instance_identifier] = @config["source"].id
            params[:target_db_instance_identifier] = @cloud_id
          end
          params[:restore_time] = @config['restore_time'] unless @config["restore_time"] == "latest"
          params[:use_latest_restorable_time] = true if @config['restore_time'] == "latest"


          MU.retrier([Aws::RDS::Errors::InvalidParameterValue], max: 15, wait: 20) {
            MU.log "Creating database #{@config['create_cluster'] ? "cluster" : "instance" } #{@cloud_id} based on point in time backup '#{@config['restore_time']}' of #{@config['source'].id}"
            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).send("restore_db_#{@config['create_cluster'] ? "cluster" : "instance"}_to_point_in_time".to_sym, params)
          }
        end

        # creation_style = new, existing and read_replica_of is not nil
        def create_read_replica
          @config["source"].kitten(@deploy)
          if !@config["source"].id
            raise MuError.new "Database '#{@config['name']}' couldn't resolve cloud id for source database", details: @config["source"].to_h
          end

          params = {
            db_instance_identifier: @cloud_id,
            source_db_instance_identifier: @config["source"].id,
            db_instance_class: @config["size"],
            auto_minor_version_upgrade: @config["auto_minor_version_upgrade"],
            publicly_accessible: @config["publicly_accessible"],
            tags: @tags.each_key.map { |k| { :key => k, :value => @tags[k] } },
            db_subnet_group_name: @config["subnet_group_name"],
            storage_type: @config["storage_type"]
          }
          if @config["source"].region and @region != @config["source"].region
            params[:source_db_instance_identifier] = MU::Cloud::AWS::Database.getARN(@config["source"].id, "db", "rds", region: @config["source"].region, credentials: @credentials)
          end

          params[:port] = @config["port"] if @config["port"]
          params[:iops] = @config["iops"] if @config['storage_type'] == "io1"

          on_retry = Proc.new { |e|
            if e.class == Aws::RDS::Errors::DBSubnetGroupNotAllowedFault
              MU.log "Being forced to use source database's subnet group: #{e.message}", MU::WARN
              params.delete(:db_subnet_group_name)
            end
          }

          MU.retrier([Aws::RDS::Errors::InvalidDBInstanceState, Aws::RDS::Errors::InvalidParameterValue, Aws::RDS::Errors::DBSubnetGroupNotAllowedFault], max: 10, wait: 30, on_retry: on_retry) {
            MU.log "Creating read replica database instance #{@cloud_id} for #{@config['source'].id}"
            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).create_db_instance_read_replica(params)
          }
        end

        # Sit on our hands until we show as available
        def wait_until_available
          loop_if = if @config["create_cluster"]
            Proc.new { cloud_desc(use_cache: false).status != "available" }
          else
            Proc.new { cloud_desc(use_cache: false).db_instance_status != "available" }
          end
          MU.retrier(wait: 10, max: 360, loop_if: loop_if) { |retries, _wait|
            if retries > 0 and retries % 20 == 0
              MU.log "Waiting for RDS #{@config['create_cluster'] ? "cluster" : "database" } #{@cloud_id} to be ready...", MU::NOTICE
            end
          }
        end

        def do_naming
          if @config["create_cluster"]
            MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: cloud_desc.db_cluster_identifier, target: "#{cloud_desc.endpoint}.", cloudclass: MU::Cloud::Database, sync_wait: @config['dns_sync_wait'])
            MU.log "Database cluster #{@config['name']} is at #{cloud_desc.endpoint}", MU::SUMMARY
          else
            MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: cloud_desc.db_instance_identifier, target: "#{cloud_desc.endpoint.address}.", cloudclass: MU::Cloud::Database, sync_wait: @config['dns_sync_wait'])
            MU.log "Database #{@config['name']} is at #{cloud_desc.endpoint.address}", MU::SUMMARY
          end
          if @config['auth_vault']
            MU.log "knife vault show #{@config['auth_vault']['vault']} #{@config['auth_vault']['item']} for Database #{@config['name']} credentials", MU::SUMMARY
          end
        end

        # Create a plain database instance or read replica, as described in our
        # +@config+.
        # @return [String]: The cloud provider's identifier for this database instance.
        def createDb

          if @config['creation_style'] == "point_in_time"
            create_point_in_time
          elsif @config['read_replica_of']
            create_read_replica
          else
            create_basic
          end

          wait_until_available
          do_naming

          # If referencing an existing DB, insert this deploy's DB security group so it can access the thing
          if @config["creation_style"] == 'existing'
            mod_config = {}
            mod_config[:db_instance_identifier] = @cloud_id
            mod_config[:vpc_security_group_ids] = cloud_desc.vpc_security_groups.map { |sg| sg.vpc_security_group_id }

            localdeploy_rule =  @deploy.findLitterMate(type: "firewall_rule", name: "database"+@config['name'])
            if localdeploy_rule.nil?
              raise MU::MuError, "Database #{@config['name']} failed to find its generic security group 'database#{@config['name']}'"
            end
            mod_config[:vpc_security_group_ids] << localdeploy_rule.cloud_id

            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).modify_db_instance(mod_config)
            MU.log "Modified database #{@cloud_id} with new security groups: #{mod_config}", MU::NOTICE
          end

          # When creating from a snapshot or replicating an existing database,
          # some of the create arguments that we'd want to carry over aren't
          # applicable- but we can apply them after the fact with a modify.
          if %w{existing_snapshot new_snapshot point_in_time}.include?(@config["creation_style"]) or @config["read_replica_of"]
            mod_config = {
              db_instance_identifier: @cloud_id,
              apply_immediately: true
            }
            if !@config["read_replica_of"] or @region == @config['source'].region
              mod_config[:vpc_security_group_ids] = @config["vpc_security_group_ids"]
            end

            if !@config["read_replica_of"]
              mod_config[:preferred_backup_window] = @config["preferred_backup_window"]
              mod_config[:backup_retention_period] = @config["backup_retention_period"]
              mod_config[:engine_version] = @config["engine_version"]
              mod_config[:allow_major_version_upgrade] = @config["allow_major_version_upgrade"] if @config['allow_major_version_upgrade']
              mod_config[:db_parameter_group_name] = @config["parameter_group_name"] if @config["parameter_group_name"]
              mod_config[:master_user_password] = @config['password']
              mod_config[:allocated_storage] = @config["storage"] if @config["storage"]
            end
            if @config["preferred_maintenance_window"]
              mod_config[:preferred_maintenance_window] = @config["preferred_maintenance_window"]
            end

            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).modify_db_instance(mod_config)
            wait_until_available
          end

          # Maybe wait for DB instance to be in available state. DB should still be writeable at this state
          if @config['allow_major_version_upgrade'] && @config["creation_style"] == "new"
            MU.log "Setting major database version upgrade on #{@cloud_id}'"

            MU::Cloud::AWS.rds(region: @region, credentials: @credentials).modify_db_instance(
              db_instance_identifier: @cloud_id,
              apply_immediately: true,
              allow_major_version_upgrade: true
            )
          end

          MU.log "Database #{@config['name']} (#{@mu_name}) is ready to use"
          @cloud_id
        end

        def run_sql_commands
          MU.log "Running initial SQL commands on #{@config['name']}", details: @config['run_sql_on_deploy']

          port = address = nil

          if !cloud_desc.publicly_accessible and @vpc
            if @config['vpc']['nat_host_name']
              keypairname, _ssh_private_key, _ssh_public_key = @deploy.SSHKey
              begin
                gateway = Net::SSH::Gateway.new(
                  @config['vpc']['nat_host_name'],
                  @config['vpc']['nat_ssh_user'],
                  :keys => [Etc.getpwuid(Process.uid).dir+"/.ssh"+"/"+keypairname],
                  :keys_only => true,
                  :auth_methods => ['publickey']
                )
                port = gateway.open(cloud_desc.endpoint.address, cloud_desc.endpoint.port)
                address = "127.0.0.1"
                MU.log "Tunneling #{@config['engine']} connection through #{@config['vpc']['nat_host_name']} via local port #{port}", MU::DEBUG
              rescue IOError => e
                MU.log "Got #{e.inspect} while connecting to #{@mu_name} through NAT #{@config['vpc']['nat_host_name']}", MU::ERR
                return
              end
            else
              MU.log "Can't run initial SQL commands! Database #{@mu_name} is not publicly accessible, but we have no NAT host for connecting to it", MU::WARN, details: @config['run_sql_on_deploy']
              return
            end
          else
            port = database.endpoint.port
            address = database.endpoint.address
          end

          # Running SQL on deploy
          if @config['engine'] =~ /postgres/
            MU::Cloud::AWS::Database.run_sql_postgres(address, port, @config['master_user'], @config['password'], cloud_desc.db_name, @config['run_sql_on_deploy'], @config['name'])
          elsif @config['engine'] =~ /mysql|maria/
            MU::Cloud::AWS::Database.run_sql_mysql(address, port, @config['master_user'], @config['password'], cloud_desc.db_name, @config['run_sql_on_deploy'], @config['name'])
          end

          # close the SQL on deploy sessions
          if !cloud_desc.publicly_accessible
            begin
              gateway.close(port)
            rescue IOError => e
              MU.log "Failed to close ssh session to NAT after running sql_on_deploy", MU::ERR, details: e.inspect
            end
          end
        end

        def self.run_sql_postgres(address, port, user, password, db, cmds = [], identifier = nil)
          identifier ||= address
          MU.log "Initiating postgres connection to #{address}:#{port} as #{user}"
          autoload :PG, 'pg'
          begin
            conn = PG::Connection.new(
              :host => address,
              :port => port,
              :user => user,
              :password => password,
              :dbname => db
            )
            cmds.each { |cmd|
              MU.log "Running #{cmd} on database #{identifier}"
              conn.exec(cmd)
            }
            conn.finish
          rescue PG::Error => e
            MU.log "Failed to run initial SQL commands on #{identifier} via #{address}:#{port}: #{e.inspect}", MU::WARN, details: conn
          end
        end
        private_class_method :run_sql_postgres

        def self.run_sql_mysql(address, port, user, password, db, cmds = [], identifier = nil)
          identifier ||= address
          autoload :Mysql, 'mysql'
          MU.log "Initiating mysql connection to #{address}:#{port} as #{user}"
          conn = Mysql.new(address, user, password, db, port)
          cmds.each { |cmd|
            MU.log "Running #{cmd} on database #{identifier}"
            conn.query(cmd)
          }
          conn.close
        end
        private_class_method :run_sql_mysql

        def self.should_delete?(tags, cloud_id, ignoremaster = false, deploy_id = MU.deploy_id, master_ip = MU.mu_public_ip, known = [])

          found_muid = false
          found_master = false
          tags.each { |tag|
            found_muid = true if tag.key == "MU-ID" && tag.value == deploy_id
            found_master = true if tag.key == "MU-MASTER-IP" && tag.value == master_ip
          }
          delete =
            if ignoremaster && found_muid
              true
            elsif !ignoremaster && found_muid && found_master
              true
            elsif known and cloud_id and known.include?(cloud_id)
              true
            else
              false
            end
          delete
        end
        private_class_method :should_delete?

        # Remove an RDS database and associated artifacts
        # @param db [OpenStruct]: The cloud provider's description of the database artifact
        # @return [void]
        def self.terminate_rds_instance(db, noop: false, skipsnapshots: false, region: MU.curRegion, deploy_id: MU.deploy_id, mu_name: nil, cloud_id: nil, credentials: nil, cluster: false, known: [])
          db ||= MU::Cloud::AWS::Database.find(cloud_id: cloud_id, region: region, credentials: credentials, cluster: cluster).values.first if cloud_id
          db_obj ||= MU::MommaCat.findStray(
            "AWS",
            "database",
            region: region,
            deploy_id: deploy_id,
            cloud_id: cloud_id,
            mu_name: mu_name,
            dummy_ok: true
          ).first
          if db_obj
            cloud_id ||= db_obj.cloud_id
            db ||= db_obj.cloud_desc
            ["parameter_group_name", "subnet_group_name"].each { |attr|
              if db_obj.config[attr]
                known ||= []
                known << db_obj.config[attr]
              end
            }
          end

          raise MuError, "terminate_rds_instance requires a non-nil database descriptor (#{cloud_id})" if db.nil? or cloud_id.nil?

          MU.retrier([], wait: 60, loop_if: Proc.new { %w{creating modifying backing-up}.include?(cluster ? db.status : db.db_instance_status) }, loop_msg: "Waiting for RDS #{cluster ? "cluster" : "instance"} #{cloud_id} to be in a valid state for deletion") {
            db = MU::Cloud::AWS::Database.find(cloud_id: cloud_id, region: region, credentials: credentials, cluster: cluster).values.first
            return if db.nil?
          }

          MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(name: cloud_id, target: (cluster ? db.endpoint : db.endpoint.address), cloudclass: MU::Cloud::Database, delete: true) if !noop

          if %w{deleting deleted}.include?(cluster ? db.status : db.db_instance_status)
            MU.log "#{cloud_id} has already been terminated", MU::WARN
          else
            params = cluster ? { :db_cluster_identifier => cloud_id } : { :db_instance_identifier => cloud_id }

            if skipsnapshots or (!cluster and (db.db_cluster_identifier or db.read_replica_source_db_instance_identifier))
              MU.log "Terminating #{cluster ? "cluster" : "database" } #{cloud_id} (not saving final snapshot)"
              params[:skip_final_snapshot] = true
            else
              MU.log "Terminating #{cluster ? "cluster" : "database" } #{cloud_id} (final snapshot: #{cloud_id}-mufinal)"
              params[:skip_final_snapshot] = false
              params[:final_db_snapshot_identifier] = "#{cloud_id}-mufinal"
            end
sleep 30
            if !noop
              on_retry = Proc.new { |e|
                if [Aws::RDS::Errors::DBSnapshotAlreadyExists, Aws::RDS::Errors::DBClusterSnapshotAlreadyExistsFault, Aws::RDS::Errors::DBClusterQuotaExceeded].include?(e.class)
                  MU.log e.message, MU::WARN
                  params[:skip_final_snapshot] = true
                  params.delete(:final_db_snapshot_identifier)
                end
              }
              MU.retrier([Aws::RDS::Errors::InvalidDBInstanceState, Aws::RDS::Errors::DBSnapshotAlreadyExists, Aws::RDS::Errors::InvalidDBClusterStateFault], wait: 60, max: 20, on_retry: on_retry) {
                if !noop
                  cluster ? MU::Cloud::AWS.rds(region: region, credentials: credentials).delete_db_cluster(params) : MU::Cloud::AWS.rds(region: region, credentials: credentials).delete_db_instance(params)
                end
              }
              del_db = nil
              MU.retrier([], wait: 10, ignoreme: [Aws::RDS::Errors::DBInstanceNotFound], loop_if: Proc.new { del_db and ((!cluster and del_db.db_instance_status != "deleted") or (cluster and del_db.status != "deleted")) }, loop_msg: "Waiting for RDS #{cluster ? "cluster" : "instance"} #{cloud_id} to delete") {
                del_db = MU::Cloud::AWS::Database.find(cloud_id: cloud_id, region: region, cluster: cluster).values.first
              }
            end
          end

          purge_rds_sgs(cloud_id, region, credentials, noop)

          purge_groomer_artifacts(db_obj, cloud_id, noop)

          MU.log "#{cloud_id} has been terminated" if !noop
        end
        private_class_method :terminate_rds_instance

        def self.purge_groomer_artifacts(db_obj, cloud_id, noop)
          return if !db_obj
          # Cleanup the database vault
          groomer = 
            if db_obj and db_obj.respond_to?(:config) and db_obj.config
              db_obj.config.has_key?("groomer") ? db_obj.config["groomer"] : MU::Config.defaultGroomer
            else
              MU::Config.defaultGroomer
            end

          groomclass = MU::Groomer.loadGroomer(groomer)
          groomclass.deleteSecret(vault: cloud_id.upcase) if !noop
        end
        private_class_method :purge_groomer_artifacts

        def self.purge_rds_sgs(cloud_id, region, credentials, noop)
          rdssecgroups = []
          begin
            secgroup = MU::Cloud::AWS.rds(region: region, credentials: credentials).describe_db_security_groups(db_security_group_name: cloud_id)
            rdssecgroups << cloud_id if !secgroup.nil?
          rescue Aws::RDS::Errors::DBSecurityGroupNotFound
            MU.log "No such RDS security group #{cloud_id} to purge", MU::DEBUG
          end

          # RDS security groups can depend on EC2 security groups, do these last
          rdssecgroups.each { |sg|
            MU.log "Removing RDS Security Group #{sg}"
            begin
              MU::Cloud::AWS.rds(region: region, credentials: credentials).delete_db_security_group(db_security_group_name: sg) if !noop
            rescue Aws::RDS::Errors::DBSecurityGroupNotFound
              MU.log "RDS Security Group #{sg} disappeared before I could remove it", MU::NOTICE
            end
          }
        end
        private_class_method :purge_rds_sgs

      end #class
    end #class
  end
end #module