cloudamatic/mu

View on GitHub
modules/mu/providers/aws/dnszone.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.

module MU

  class Cloud
    class AWS
      # A DNS Zone as configured in {MU::Config::BasketofKittens::dnszones}
      class DNSZone < MU::Cloud::DNSZone

        # 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
          @mu_name ||= @deploy.getResourceName(@config["name"])

          MU.setVar("curRegion", @region) if !@region.nil?
        end

        # Called automatically by {MU::Deploy#createResources}
        def create
          ext_zone = MU::Cloud::DNSZone.find(cloud_id: @config['name']).values.first
          @config["create_zone"] =
            if ext_zone
              false
            else
              true
            end

          if @config["create_zone"]
            params = {
              :name => @config['name'],
              :hosted_zone_config => {
                :comment => @deploy.deploy_id
              },
              :caller_reference => @deploy.getResourceName(@config['name'])
            }

            # Private zones have their lookup restricted by VPC
            add_vpcs = []
            if @config['private']
              if @config['all_account_vpcs']
                # If we've been told to make this domain available account-wide, do so
                MU::Cloud::AWS.listRegions(@config['us_only']).each { |region|
                  known_vpcs = MU::Cloud::AWS.ec2(region: region).describe_vpcs.vpcs

                  MU.log "Enumerating VPCs in #{region}", MU::DEBUG, details: known_vpcs

                  known_vpcs.each { |vpc|
                    add_vpcs << { :vpc_id => vpc.vpc_id, :region => region }
                  }
                }
              else
                # Or if we were given a list of VPCs add them
                raise MuError, "DNS Zone #{@config['name']} is flagged as private, you must either provide a VPC, or set 'all_account_vpcs' to true" if @config['vpcs'].nil? || @config['vpcs'].empty?
                @config['vpcs'].each { |vpc|
                  add_vpcs << { :vpc_id => vpc['vpc_id'], :region => vpc['region'] }
                }
              end

              raise MuError, "DNS Zone #{@config['name']} is flagged as private, but I can't find any VPCs in which to put it" if add_vpcs.empty?

              # We can only specify one VPC when creating a private zone. We'll add the rest later
              params[:vpc] = {
                :vpc_region => add_vpcs.first[:region],
                :vpc_id => add_vpcs.first[:vpc_id]
              }
            end

            MU.log "Creating DNS Zone '#{@config['name']}'", details: params

            resp = MU::Cloud::AWS.route53.create_hosted_zone(params)
            id = resp.hosted_zone.id
            @config['zone_id'] = id

            begin
              resp = MU::Cloud::AWS.route53.get_hosted_zone(id: id)
              sleep 10
            end while resp.nil? or resp.size == 0

            if !add_vpcs.empty?
              add_vpcs.each { |vpc|
                if vpc[:vpc_id] != params[:vpc][:vpc_id]
                  MU.log "Associating VPC #{vpc[:vpc_id]} in #{vpc[:region]} with DNS Zone #{@config['name']}", MU::DEBUG
                  begin
                    MU::Cloud::AWS.route53.associate_vpc_with_hosted_zone(
                      hosted_zone_id: id,
                      vpc: {
                        :vpc_region => vpc[:region],
                        :vpc_id => vpc[:vpc_id]
                      }
                    )
                  rescue Aws::Route53::Errors::InvalidVPCId => e
                    MU.log "Unable to associate #{vpc[:vpc_id]} in #{vpc[:region]} with DNS Zone #{@config['name']}: #{e.inspect}", MU::WARN
                  end
                end
              }
            end
          end

          @config['records'] = [] if !@config['records']
          @config['records'].each { |dnsrec|
            dnsrec['name'] = "#{dnsrec['name']}.#{MU.environment.downcase}" if dnsrec["append_environment_name"] && !dnsrec['name'].match(/\.#{MU.environment.downcase}$/)

            if dnsrec.has_key?('mu_type')
              dnsrec['target'] =
                if dnsrec['mu_type'] == "loadbalancer"
                  if @dependencies.has_key?('loadbalancer') and @dependencies['loadbalancer'].has_key?(dnsrec['target']) and !@dependencies['loadbalancer'][dnsrec['target']].cloudobj.nil? and dnsrec['deploy_id'].nil?
                    @dependencies['loadbalancer'][dnsrec['target']].cloudobj.notify['dns']
                  elsif dnsrec['deploy_id']
                    found = MU::MommaCat.findStray("AWS", "loadbalancer", deploy_id: dnsrec["deploy_id"], mu_name: dnsrec["target"], region: @region)
                    raise MuError, "Couldn't find #{dnsrec['mu_type']} #{dnsrec["target"]}" if found.nil? || found.empty?
                    found.first.deploydata['dns']
                  end
                elsif dnsrec['mu_type'] == "server"
                  if @dependencies.has_key?(dnsrec['mu_type']) && dnsrec['deploy_id'].nil?
                    MU.log "dnsrec['target'] #{dnsrec['target']}"
                    deploydata = @dependencies['server'][dnsrec['target']].deploydata
                  elsif dnsrec['deploy_id']
                    found = MU::MommaCat.findStray("AWS", "server", deploy_id: dnsrec["deploy_id"], mu_name: dnsrec["target"], region: @region)
                    raise MuError, "Couldn't find #{dnsrec['mu_type']} #{dnsrec["target"]}" if found.nil? || found.empty?
                    deploydata = found.first.deploydata
                  end

                  public = true
                  if dnsrec.has_key?("target_type")
                    public = dnsrec["target_type"] == "private" ? false : true
                  end

                  if dnsrec["type"] == "CNAME"
                    if public
                      # Make sure we have a public canonical name to register. Use the private one if we don't
                      deploydata['public_dns_name'].empty? ? deploydata['private_dns_name'] : deploydata['public_dns_name']
                    else
                      # If we specifically requested to register the private canonical name lets use that
                      deploydata['private_dns_name']
                    end
                  elsif dnsrec["type"] == "A"
                    if public
                      # Make sure we have a public IP address to register. Use the private one if we don't
                      deploydata['public_ip_address'] ? deploydata['public_ip_address'] : deploydata['private_ip_address']
                    else
                      # If we specifically requested to register the private IP lets use that
                      deploydata['private_ip_address']
                    end
                  end
                elsif dnsrec['mu_type'] == "database"
                  if @dependencies.has_key?(dnsrec['mu_type']) && dnsrec['deploy_id'].nil?
                    @dependencies[dnsrec['mu_type']][dnsrec['target']].deploydata['endpoint']
                  elsif dnsrec['deploy_id']
                    found = MU::MommaCat.findStray("AWS", "database", deploy_id: dnsrec["deploy_id"], mu_name: dnsrec["target"], region: @region)
                    raise MuError, "Couldn't find #{dnsrec['mu_type']} #{dnsrec["target"]}" if found.nil? || found.empty?
                    found.first.deploydata['endpoint']
                  end
                end
              end

            dnsrec["zone"] = {"name" => @config['name']}
          }

          MU::Cloud::AWS::DNSZone.createRecordsFromConfig(@config['records'])
          return resp.hosted_zone if @config["create_zone"]
        end

        # Resolve a record entry (as in {MU::Config::BasketofKittens::dnszones::records} to the full DNS name we would assign it
        def self.recordToName(record)
          shortname = record['name']
          shortname += ".#{MU.environment.downcase}" if record["append_environment_name"]

          zone = if record['zone'].has_key?("id")
            MU::Cloud::DNSZone.find(cloud_id: record['zone']['id']).values.first
          else
            MU::Cloud::DNSZone.find(cloud_id: record['zone']['name']).values.first
          end

          if zone.nil?
            raise MuError.new "Failed to locate Route53 DNS Zone", details: record['zone']
          end

          shortname+"."+zone.name.sub(/\.$/, '')
        end

        # Wrapper for {MU::Cloud::AWS::DNSZone.manageRecord}. Spawns threads to create all
        # requested records in background and returns immediately.
        # @param cfg [Array]: An array of parsed {MU::Config::BasketofKittens::dnszones::records} objects.
        # @param target [String]: Optional target for the records to be created. Overrides targets embedded in cfg records.
        def self.createRecordsFromConfig(cfg, target: nil, name_only: false)
          return if cfg.nil?
          record_threads = []

          cfg.each { |record|
            record['name'] = "#{record['name']}.#{MU.environment.downcase}" if record["append_environment_name"] && !record['name'].match(/\.#{MU.environment.downcase}$/)
            zone = nil
            if record['zone'].has_key?("id")
              zone = MU::Cloud::DNSZone.find(cloud_id: record['zone']['id']).values.first
            else
              zone = MU::Cloud::DNSZone.find(cloud_id: record['zone']['name']).values.first
            end


            healthcheck_id = nil
            record['target'] = target if !target.nil?
            child_check_ids = []
            if record.has_key?('healthchecks')
              record['healthchecks'].each { |check|
                child_check_ids << MU::Cloud::AWS::DNSZone.createHealthCheck(check, record['target']) if check['type'] == "secondary"
              }

              record['healthchecks'].each { |check|
                if check['type'] == "primary"
                  check["health_check_ids"] = child_check_ids if !check.has_key?("health_check_ids") || check['health_check_ids'].empty?
                  healthcheck_id = MU::Cloud::AWS::DNSZone.createHealthCheck(check, record['target'])
                  break
                end
              }
            end

            # parent_thread_id seems to be nil sometimes, try to make sure we don't fail
            # There has got to be a better way to deal with this than this
            parent_thread_id = Thread.current.object_id
            while parent_thread_id.nil?
              parent_thread_id = Thread.current.object_id
              sleep 3
            end

            record_threads << Thread.new {
              MU.dupGlobals(parent_thread_id)
              MU::Cloud::AWS::DNSZone.manageRecord(
                zone.id,
                record['name'],
                record['type'],
                targets: [record['target']],
                ttl: record['ttl'],
                failover: record['failover'],
                healthcheck: healthcheck_id,
                weight: record['weight'],
                overwrite: record['override_existing'],
                location: record['geo_location'],
                region: record['region'],
                alias_zone: record['alias_zone'],
                sync_wait: false
              )
            }
          }

          record_threads.each { |t|
            t.join
          }
        end

        # Create a Route53 health check.
        # @param cfg [Hash]: Parsed hash of {MU::Config::BasketofKittens::dnszones::records::healthchecks}
        # @param target [String]: The IP address of FQDN of the target resource to check.
        def self.createHealthCheck(cfg, target)
          check = {
            type: cfg['method'],
            inverted: cfg['inverted']
          }
          
          if cfg['method'] == "CALCULATED"
            check[:health_threshold] = cfg['health_threshold'] if cfg.has_key?('health_threshold')
            check[:child_health_checks] = cfg['health_check_ids'] if cfg.has_key?('health_check_ids')
          elsif cfg['method'] == "CLOUDWATCH_METRIC"
            check[:insufficient_data] = cfg['insufficient_data'] if cfg.has_key?('insufficient_data')
            check[:alarm_identifier] = {
              region: cfg['alarm_region'],
              name: cfg['alarm_name']
            }
          else
            check[:resource_path] = cfg['path'] if cfg.has_key?('path')
            check[:search_string] = cfg['search_string'] if cfg.has_key?('search_string')
            check[:port] = cfg['port'] if cfg.has_key?('port')
            check[:enable_sni] = cfg['enable_sni'] if cfg.has_key?('enable_sni')
            check[:regions] = cfg['regions'] if cfg.has_key?('regions')
            check[:measure_latency] = cfg['latency'] if cfg.has_key?('latency')
            check[:check_interval] = cfg['check_interval']
            check[:failure_threshold] = cfg['failure_threshold']

            if target.match(/^\d+\.\d+\.\d+\.\d+$/)
              check[:ip_address] = target
            else
              check[:fully_qualified_domain_name] = target
            end
          end

          MU.log "Creating health check for #{cfg['name']}", details: check
          id = MU::Cloud::AWS.route53.create_health_check(
              caller_reference: "#{MU.deploy_id}-#{cfg['method']}-#{cfg['name']}-#{Time.now.to_i.to_s}",
              health_check_config: check
          ).health_check.id

          # Currently the only thing we can tag in Route 53... is health checks.
          tags = []
          MU::MommaCat.listStandardTags.each_pair { |name, value|
            tags << {key: name, value: value}
          }

          tags << {key: "Name", value: "#{MU.deploy_id}-#{cfg['name']}".upcase}

          if cfg['optional_tags']
            MU::MommaCat.listOptionalTags.each_pair { |name, value|
              tags << {key: name, value: value}
            }
          end

          if cfg['tags']
            cfg['tags'].each { |tag|
              tags << {key: tag['key'], value: tag['value']}
            }
          end

          MU::Cloud::AWS.route53.change_tags_for_resource(
              resource_type: "healthcheck",
              resource_id: id,
              add_tags: tags
          )

          return id
        end


        # Add or remove access for a given (presumably) private cloud-hosted DNS
        # zone to/from the specified VPC.
        # @param id [String]: The cloud identifier of the DNS zone to update
        # @param vpc_id [String]: The cloud identifier of the VPC
        # @param region [String]: The cloud provider's region
        # @param remove [Boolean]: Whether to remove access (default: grant access)
        def self.toggleVPCAccess(id: nil, vpc_id: nil, region: MU.curRegion, remove: false, credentials: nil)

          if !remove
            MU.log "Granting VPC #{vpc_id} access to zone #{id}"
            MU::Cloud::AWS.route53(credentials: credentials).associate_vpc_with_hosted_zone(
                hosted_zone_id: id,
                vpc: {
                    :vpc_id => vpc_id,
                    :vpc_region => region
                },
                comment: MU.deploy_id
            )
          else
            MU.log "Revoking VPC #{vpc_id} access to zone #{id}"
            begin
              MU::Cloud::AWS.route53(credentials: credentials).disassociate_vpc_from_hosted_zone(
                  hosted_zone_id: id,
                  vpc: {
                      :vpc_id => vpc_id,
                      :vpc_region => region
                  },
                  comment: MU.deploy_id
              )
            rescue Aws::Route53::Errors::LastVPCAssociation => e
              MU.log e.inspect, MU::WARN
            rescue Aws::Route53::Errors::VPCAssociationNotFound
              MU.log "VPC #{vpc_id} access to zone #{id} already revoked", MU::NOTICE
            end
          end
        end

        # Create a new DNS record in the given DNS zone
        # @param id [String]: The cloud provider's identifier for the zone.
        # @param name [String]: The DNS name we're creating
        # @param type [String]: The class of DNS record we're creating (e.g. A, CNAME, PTR, SPF...)
        # @param targets [Array<String>]: Standard DNS values for this record. Must be valid for the 'type' field, e.g. A records must point to a IP addresses.
        # @param ttl [Integer]: The DNS time-to-live value for this record.
        # @param delete [Boolean]: Whether to delete the described record, instead of creating.
        # @param overwrite [Boolean]: Whether to overwrite existing records which match this description, as opposed to creating an entirely new one.
        # @param sync_wait [Boolean]: Wait until the record change has fully propagated throughout Route53 before returning.
        # @param failover [String]: "PRIMARY" or "SECONDARY" for Route53 failover. See also {MU::Config::BasketofKittens::dnszones::records}.
        # @param healthcheck [String]: A Route53 healthcheck identifier for use with failover. Typically created by {MU::Config::BasketofKittens::dnszones::records::healthchecks}.
        # @param region [String]: An Amazon Web Services region for use with latency-based routing. See also {MU::Config::BasketofKittens::dnszones::records}.
        # @param weight [Integer]: A weight value used for weighted routing, used to determine proportion of traffic with other matching weighted records. See also {MU::Config::BasketofKittens::dnszones::records}.
        # @param location [Hash<String>]: A parsed Hash of {MU::Config::BasketofKittens::dnszones::records::geo_location}.
        # @param set_identifier [String]: A unique string to differentiate otherwise-similar records. Normally auto-generated, should not need to specify.
        # @param alias_zone [String]: Zone ID of the target's hosted zone, when creating an alias (type R53ALIAS)
        def self.manageRecord(id, name, type, targets: nil,
            ttl: 7200, delete: false, sync_wait: true, failover: nil,
            healthcheck: nil, region: nil, weight: nil, overwrite: true,
            location: nil, set_identifier: nil, alias_zone: nil, noop: false)

          MU.setVar("curRegion", region) if !region.nil?
          zone = MU::Cloud::DNSZone.find(cloud_id: id).values.first
          raise MuError, "Attempting to add record to nonexistent DNS zone #{id}" if zone.nil?
          name = name + "." + zone.name if !name.match(/(^|\.)#{zone.name}$/)

          action = "CREATE"
          action = "UPSERT" if overwrite
          action = "DELETE" if delete

          record_sets = MU::Cloud::AWS.route53.list_resource_record_sets(
            hosted_zone_id: id,
            start_record_name: name
          ).resource_record_sets if delete

          if type == "R53ALIAS"
            target_zone = id
            target_name = targets[0].downcase
            target_name.chomp!(".")

            if !alias_zone.nil?
              target_zone = "/hostedzone/"+alias_zone if !alias_zone.match(/^\/hostedzone\//)
            else
              MU::Cloud::AWS.listRegions.each { |r|
                MU::Cloud::AWS.elb(region: r).describe_load_balancers.load_balancer_descriptions.each { |elb|
                  elb_dns = elb.dns_name.downcase
                  elb_dns.chomp!(".")
                  if target_name == elb_dns
                    MU.log "Resolved #{targets[0]} to an Elastic Load Balancer in zone #{elb.canonical_hosted_zone_name_id}", details: elb
                    target_zone = "/hostedzone/"+elb.canonical_hosted_zone_name_id
                    break
                  end
                }
                break if target_zone != id
              }
            end

            base_rrset = {
              name: name,
              type: "A",
              alias_target: {
                hosted_zone_id: target_zone,
                dns_name: targets[0],
                evaluate_target_health: true
              }
            }
          else
            rrsets = []
            if delete
              record_sets.each { |r|
                if r.name == name and r.type == type
                  rrsets = MU.structToHash(r.resource_records)
                end
              }
            end

            if !targets.nil? and (!delete or rrsets.empty?)
              targets.each { |target|
                rrsets << {value: target}
              }
            end

            base_rrset = {
              name: name,
              type: type,
              ttl: ttl,
              resource_records: rrsets
            }


            if !healthcheck.nil?
              base_rrset[:health_check_id] = healthcheck
            end
          end

          params = {
            hosted_zone_id: id,
            change_batch: {
              changes: [
                {
                  action: action,
                  resource_record_set: base_rrset
                }
              ]
            }
          }

          # Doing an UPSERT with a new set_identifier will fail with a record already exist error, so lets try and get it from an existing record. 
          # This can be an issue with multiple secondary failover records
          if (location || failover || region || weight) and set_identifier.nil?
            record_sets ||= MU::Cloud::AWS.route53.list_resource_record_sets(
              hosted_zone_id: id,
              start_record_name: name
            ).resource_record_sets

          
            record_sets.each { |r|
              if r.name == name
                if location && location == r.location
                  set_identifier = r.set_identifier
                  break
                elsif failover && failover == r.failover
                  set_identifier = r.set_identifier
                  break
                elsif region && region == r.region
                  set_identifier = r.set_identifier
                  break
                elsif weight && weight == r.weight
                  set_identifier = r.set_identifier
                  break
                end
              end
            }
          end

          if !failover.nil?
            base_rrset[:failover] = failover
            set_identifier ||= "#{MU.deploy_id}-failover-#{failover}".upcase
          elsif !weight.nil?
            base_rrset[:weight] = weight
            set_identifier ||= "#{MU.deploy_id}-weighted-#{weight.to_s}".upcase
          elsif !location.nil?
            loc_arg = Hash.new
            location.each_pair { |key, val|
              sym = key.to_sym
              loc_arg[sym] = val
            }
            base_rrset[:geo_location] = loc_arg
            set_identifier ||= "#{MU.deploy_id}-location-#{location.values.join("-")}".upcase
          elsif !region.nil?
            base_rrset[:region] = region
            set_identifier ||= "#{MU.deploy_id}-latency-#{region}".upcase
          end

          base_rrset[:set_identifier] = set_identifier if set_identifier

          if delete
            MU.log "Deleting DNS record #{name} (#{type}) from #{id}", details: params
          else
            MU.log "Adding DNS record #{name} => #{targets} (#{type}) to #{id}", details: params
          end

          return if noop

          on_retry = Proc.new { |e|
            if (delete and e.message.match(/but it was not found/)) or
               (!delete and e.message.match(/(it|name) already exists/))
              MU.log e.message, MU::DEBUG, details: params
              return
            elsif e.class == Aws::Route53::Errors::InvalidChangeBatch
              MU.log "Problem managing entry for #{name}", MU::ERR, details: params
              raise MuError, e.inspect
            end
          }

          change_id = nil
          MU.retrier([Aws::Route53::Errors::PriorRequestNotComplete, Aws::Route53::Errors::InvalidChangeBatch], wait: 15, max: 10, on_retry: on_retry) {
            change_id = MU::Cloud::AWS.route53.change_resource_record_sets(params).change_info.id
          }

          if sync_wait
            attempts = 0
            start_time = Time.now.to_i
            begin
              MU.log "Waiting for DNS record change for '#{name}' to propagate in zone '#{zone.name}'", MU::NOTICE if attempts % 3 == 0
              sleep 15
              change_info = MU::Cloud::AWS.route53.get_change(id: change_id).change_info
              if change_info.status != "INSYNC" and attempts % 3 == 0
                MU.log "DNS zone #{zone.name} still in state #{change_info.status} after #{Time.now.to_i - start_time}s", MU::DEBUG, details: change_info
              end
              attempts = attempts + 1
            end while change_info.status != "INSYNC"
          end
        end

        #        @resolver = Resolv::DNS.new

        # Set a generic .platform-mu DNS entry for a resource, and return the name that
        # was set.
        # @param name [name]: The base name of the resource
        # @param target [String]: The target of the DNS entry, usually an IP.
        # @param noop [Boolean]: Don't attempt to adjust entries, just return the name we'd create/remove.
        # @param delete [Boolean]: Remove this entry instead of creating it.
        # @param cloudclass [Object]: The resource's Mu class.
        # @param sync_wait [Boolean]: Wait for DNS entry to propagate across zone.
        def self.genericMuDNSEntry(name: nil, target: nil, cloudclass: nil, noop: false, delete: false, sync_wait: true, credentials: nil)
          return nil if name.nil? or cloudclass.nil?
          return nil if target.nil? and !delete
          mu_zone = MU::Cloud::DNSZone.find(cloud_id: "platform-mu", credentials: credentials).values.first
          raise MuError, "Couldn't isolate platform-mu DNS zone" if mu_zone.nil?

          if !mu_zone.nil? and !MU.myVPC.nil?
            subdomain = cloudclass.cfg_name
            dns_name = name.downcase+"."+subdomain
            dns_name += "."+MU.myInstanceId if MU.myInstanceId

            record_type = "CNAME"
            record_type = "A" if target.match(/^\d+\.\d+\.\d+\.\d+/)
            ip = nil

            records = []
            lookup = MU::Cloud::AWS.route53(credentials: credentials).list_resource_record_sets(
              hosted_zone_id: mu_zone.id,
              start_record_name: "#{dns_name}.platform-mu",
              start_record_type: record_type,
              max_items: 1
            ).resource_record_sets

            lookup.each { |record|
              if record.name.match(/^#{dns_name}\.platform-mu/i) and record.type == record_type
                record.resource_records.each { |rrset|
                  if rrset.value == target
                    ip = rrset.value
                  end
                }

              end
            }

#                    begin
#                        ip = @resolver.getaddress("#{dns_name}.platform-mu")
#MU.log "@resolver.getaddress(#{dns_name}.platform-mu) => #{ip.to_s} (target is #{target})", MU::WARN, details: ip
#                    rescue Resolv::ResolvError => e
#                        MU.log "'#{dns_name}.platform-mu' does not resolve.", MU::DEBUG, details: e.inspect
#                    end

            if ip == target and !delete
              return "#{dns_name}.platform-mu"
            end

            sync_wait = false if delete

            record_type = "R53ALIAS" if cloudclass == MU::Cloud::AWS::LoadBalancer
            MU::Cloud::AWS::DNSZone.manageRecord(mu_zone.id, dns_name, record_type, targets: [target], delete: delete, sync_wait: sync_wait, noop: noop)
            return "#{dns_name}.platform-mu"
          else
            return nil
          end
        end

        # Log DNS zone metadata to the deployment struct for the current deploy.
        def notify
          if @config["create_zone"]
# # XXX this wants generalization
            # if !@deploy.deployment[MU::Cloud::DNSZone.cfg_plural].nil? and !@deploy.deployment[MU::Cloud::DNSZone.cfg_plural][name].nil?
              # deploydata = @deploy.deployment[MU::Cloud::DNSZone.cfg_plural][name].dup
            # else
              # deploydata = Hash.new
            # end

            # resp = MU::Cloud::AWS.route53.get_hosted_zone(
                # id: @config['zone_id']
            # )
            # deploydata.merge!(MU.structToHash(resp.hosted_zone))
            # deploydata['vpcs'] = @config['vpcs'] if !@config['vpcs'].nil?
            # deploydata["region"] = @region if !@region.nil?
            # @deploy.notify(MU::Cloud::DNSZone.cfg_plural, mu_name, deploydata)
            # return deploydata

            resp = MU::Cloud::AWS.route53.get_hosted_zone(id: @config['zone_id'])
            vpcs = []
            hosted_zone_vpcs = resp.vp_cs
            if !hosted_zone_vpcs.empty?
              hosted_zone_vpcs.each{ |vpc|
                vpcs << vpc.to_h
              }
            end

            {
              "name" => resp.hosted_zone.name,
              "id" => resp.hosted_zone.id,
              "private" => resp.hosted_zone.config.private_zone,
              "vpcs" => vpcs,
            }

          else
            # We should probably return the records we created
            {}
          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?
          true
        end

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

        # Called by {MU::Cleanup}. Locates resources that were created by the
        # currently-loaded deployment, and purges them.
        def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
          MU.log "AWS::DNSZone.cleanup: need to support flags['known']", MU::DEBUG, details: flags

          threads = []
          MU::Cloud::AWS.route53(credentials: credentials).list_health_checks.health_checks.each { |check|
            begin
              tags = MU::Cloud::AWS.route53(credentials: credentials).list_tags_for_resource(
                  resource_type: "healthcheck",
                  resource_id: check.id
              ).resource_tag_set.tags
              muid_match = false
              mumaster_match = false
              tags.each { |tag|
                muid_match = true if tag.key == "MU-ID" and tag.value == deploy_id
                mumaster_match = true if tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
              }

              delete = false
              if muid_match
                if ignoremaster
                  delete = true
                else
                  delete = true if mumaster_match
                end
              end

              if delete
                parent_thread_id = Thread.current.object_id
                threads << Thread.new(check) { |mycheck|
                  MU.dupGlobals(parent_thread_id)
                  Thread.abort_on_exception = true
                  MU.log "Removing health check #{mycheck.id}"
                  retries = 5
                  begin 
                    MU::Cloud::AWS.route53(credentials: credentials).delete_health_check(health_mycheck_id: mycheck.id) if !noop
                  rescue Aws::Route53::Errors::NoSuchHealthCheck => e
                    MU.log "Health Check '#{mycheck.id}' disappeared before I could remove it", MU::WARN, details: e.inspect
                  rescue Aws::Route53::Errors::InvalidInput => e
                    if e.message.match(/is still referenced from parent health check/) && retries <= 5
                      sleep 5
                      retries += 1
                      retry
                    else
                      MU.log "Health Check #{mycheck.id} still has a parent health check associated with it, skipping", MU::WARN, details: e.inspect
                    end
                  end
                }
              end
            rescue Aws::Route53::Errors::NoSuchHealthCheck => e
              MU.log "Health Check '#{check.id}' disappeared before I could remove it", MU::WARN, details: e.inspect
            end
          }

          threads.each { |t|
            t.join
          }

          zones = MU::Cloud::DNSZone.find(deploy_id: deploy_id, region: region)
          zones.values.each { |zone|
            MU.log "Purging DNS Zone '#{zone.name}' (#{zone.id})"
            if !noop
              begin
                # Clean up resource records first
                rrsets = MU::Cloud::AWS.route53(credentials: credentials).list_resource_record_sets(hosted_zone_id: zone.id)
                rrsets.resource_record_sets.each { |rrset|
                  next if zone.name == rrset.name and (rrset.type == "NS" or rrset.type == "SOA")
                  MU::Cloud::AWS.route53(credentials: credentials).change_resource_record_sets(
                      hosted_zone_id: zone.id,
                      change_batch: {
                          changes: [
                              {
                                  action: "DELETE",
                                  resource_record_set: MU.structToHash(rrset)
                              }
                          ]
                      }
                  )
                }

                MU::Cloud::AWS.route53(credentials: credentials).delete_hosted_zone(id: zone.id)
              rescue Aws::Route53::Errors::PriorRequestNotComplete
                MU.log "Still waiting for all records in DNS Zone '#{zone.name}' (#{zone.id}) to delete", MU::WARN
                sleep 20
                retry
              rescue Aws::Route53::Errors::InvalidChangeBatch
                # Just skip this
              rescue Aws::Route53::Errors::NoSuchHostedZone => e
                MU.log "DNS Zone '#{zone.name}' (#{zone.id}) disappeared before I could remove it", MU::WARN, details: e.inspect
              rescue Aws::Route53::Errors::HostedZoneNotEmpty => e
                raise MuError, e.inspect
              end
            end
          }

          # Lets try cleaning MU DNS records in all zones.
          MU::Cloud::AWS.route53(credentials: credentials).list_hosted_zones.hosted_zones.each { |zone|
            begin 
              zone_rrsets = []
              rrsets = MU::Cloud::AWS.route53(credentials: credentials).list_resource_record_sets(hosted_zone_id: zone.id)
              rrsets.resource_record_sets.each { |record|
                zone_rrsets << record
              }

              # AWS API returns a maximum of 100 results. DNS zones are likely to have more than 100 records, lets page and make sure we grab all records in a given zone
              while rrsets.next_record_name && rrsets.next_record_type
                rrsets = MU::Cloud::AWS.route53(credentials: credentials).list_resource_record_sets(hosted_zone_id: zone.id, start_record_name: rrsets.next_record_name, start_record_type: rrsets.next_record_type)
                rrsets.resource_record_sets.each { |record|
                  zone_rrsets << record
                }
              end

              # TO DO: if we have more than one record it will retry the deletion multiple times and will throw Aws::Route53::Errors::InvalidChangeBatch / record not found even though the record was deleted
              zone_rrsets.each { |record|
                if record.name.match(deploy_id.downcase)
                  resource_records = []
                  record.resource_records.each { |rrecord|
                    resource_records << rrecord.value
                  }

                  MU::Cloud::AWS::DNSZone.manageRecord(zone.id, record.name, record.type, targets: resource_records, ttl: record.ttl, sync_wait: false, delete: true) if !noop
                end
              }
            rescue Aws::Route53::Errors::NoSuchHostedZone
              MU.log "DNS Zone '#{zone.name}' #{zone.id} disappeared while was looking at", MU::WARN
            end
          }
        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 = []
          schema = {}
          [toplevel_required, schema]
        end

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

          if !zone["records"].nil?
            zone["records"].each { |record|
              record['scrub_mu_isms'] = zone['scrub_mu_isms'] if zone.has_key?('scrub_mu_isms')
              route_types = 0
              route_types = route_types + 1 if !record['weight'].nil?
              route_types = route_types + 1 if !record['geo_location'].nil?
              route_types = route_types + 1 if !record['region'].nil?
              route_types = route_types + 1 if !record['failover'].nil?
  
              if route_types > 1
                MU.log "At most one of weight, location, region, and failover can be specified in a record.", MU::ERR, details: record
                ok = false
              end
  
              if !record['mu_type'].nil?
                MU::Config.addDependency(zone, record['target'], record['mu_type'])
              end
  
              if record.has_key?('healthchecks') && !record['healthchecks'].empty?
                primary_alarms_set = []
                record['healthchecks'].each { |check|
                  check['alarm_region'] ||= zone['region'] if check['method'] == "CLOUDWATCH_METRIC"
                  primary_alarms_set << true if check['type'] == 'primary'
                }
  
                if primary_alarms_set.size != 1
                  MU.log "Must have only one primary health check, but #{primary_alarms_set.size} are set.", MU::ERR, details: record
                  ok = false
                end
  
                # record['healthcheck']['alarm_region'] ||= zone['region'] if record['healthcheck']['method'] == "CLOUDWATCH_METRIC"
  
                if route_types == 0
                  MU.log "Health check in a DNS zone only valid with Weighted, Location-based, Latency-based, or Failover routing.", MU::ERR, details: record
                  ok = false
                end
              end
  
              if !record['geo_location'].nil?
                if !record['geo_location']['continent_code'].nil? and (!record['geo_location']['country_code'].nil? or !record['geo_location']['subdivision_code'].nil?)
                  MU.log "Location routing cannot mix continent_code with other location specifiers.", MU::ERR, details: record
                  ok = false
                end
                if record['geo_location']['country_code'].nil? and !record['geo_location']['subdivision_code'].nil?
                  MU.log "Cannot specify subdivision_code without country_code.", MU::ERR, details: record
                  ok = false
                end
              end
            }
          end

          ok
        end

        # Canonical Amazon Resource Number for this resource
        # @return [String]
        def arn
          nil # no such animal in Route53
        end

        # Locate an existing DNSZone or DNSZones 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 DNSZones
        def self.find(**args)
          matches = {}

          resp = MU::Cloud::AWS.route53(credentials: args[:credentials]).list_hosted_zones(
              max_items: 100
          )

          resp.hosted_zones.each { |zone|
            if !args[:cloud_id].nil? and !args[:cloud_id].empty?
              if zone.id == args[:cloud_id]
                begin 
                  matches[zone.id] = MU::Cloud::AWS.route53(credentials: args[:credentials]).get_hosted_zone(id: zone.id).hosted_zone
                rescue Aws::Route53::Errors::NoSuchHostedZone
                  MU.log "Hosted zone #{zone.id} doesn't exist"
                end
              elsif zone.name == args[:cloud_id] or zone.name == args[:cloud_id]+"."
                begin 
                  matches[zone.id] = MU::Cloud::AWS.route53(credentials: args[:credentials]).get_hosted_zone(id: zone.id).hosted_zone
                rescue Aws::Route53::Errors::NoSuchHostedZone
                  MU.log "Hosted zone #{zone.id} doesn't exist"
                end
              end
            end
            if !args[:deploy_id].nil? and !args[:deploy_id].empty? and zone.config.comment == args[:deploy_id]
              begin 
                matches[zone.id] = MU::Cloud::AWS.route53(credentials: args[:credentials]).get_hosted_zone(id: zone.id).hosted_zone
              rescue Aws::Route53::Errors::NoSuchHostedZone
                MU.log "Hosted zone #{zone.id} doesn't exist"
              end
            end
          }

          return matches
        end
      end
    end
  end
end