class MU::Cloud::AWS::DNSZone

A DNS Zone as configured in {MU::Config::BasketofKittens::dnszones}

Public Class Methods

cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {}) click to toggle source

Called by {MU::Cleanup}. Locates resources that were created by the currently-loaded deployment, and purges them.

# File modules/mu/providers/aws/dnszone.rb, line 686
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
createHealthCheck(cfg, target) click to toggle source

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.

# File modules/mu/providers/aws/dnszone.rb, line 265
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
createRecordsFromConfig(cfg, target: nil, name_only: false) click to toggle source

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.

# File modules/mu/providers/aws/dnszone.rb, line 198
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
find(**args) click to toggle source

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

# File modules/mu/providers/aws/dnszone.rb, line 892
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
genericMuDNSEntry(name: nil, target: nil, cloudclass: nil, noop: false, delete: false, sync_wait: true, credentials: nil) click to toggle source

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.

# File modules/mu/providers/aws/dnszone.rb, line 575
        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
isGlobal?() click to toggle source

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

# File modules/mu/providers/aws/dnszone.rb, line 674
def self.isGlobal?
  true
end
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) click to toggle source

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)

# File modules/mu/providers/aws/dnszone.rb, line 386
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
new(**args) click to toggle source

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

Calls superclass method
# File modules/mu/providers/aws/dnszone.rb, line 24
def initialize(**args)
  super
  @mu_name ||= @deploy.getResourceName(@config["name"])

  MU.setVar("curRegion", @region) if !@region.nil?
end
quality() click to toggle source

Denote whether this resource implementation is experiment, ready for testing, or ready for production use.

# File modules/mu/providers/aws/dnszone.rb, line 680
def self.quality
  MU::Cloud::RELEASE
end
recordToName(record) click to toggle source

Resolve a record entry (as in {MU::Config::BasketofKittens::dnszones::records} to the full DNS name we would assign it

# File modules/mu/providers/aws/dnszone.rb, line 177
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
schema(_config) click to toggle source

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

# File modules/mu/providers/aws/dnszone.rb, line 817
def self.schema(_config)
  toplevel_required = []
  schema = {}
  [toplevel_required, schema]
end
toggleVPCAccess(id: nil, vpc_id: nil, region: MU.curRegion, remove: false, credentials: nil) click to toggle source

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)

# File modules/mu/providers/aws/dnszone.rb, line 339
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
validateConfig(zone, _configurator) click to toggle source

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

# File modules/mu/providers/aws/dnszone.rb, line 827
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

Public Instance Methods

arn() click to toggle source

Canonical Amazon Resource Number for this resource @return [String]

# File modules/mu/providers/aws/dnszone.rb, line 886
def arn
  nil # no such animal in Route53
end
create() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/aws/dnszone.rb, line 32
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
notify() click to toggle source

Log DNS zone metadata to the deployment struct for the current deploy.

# File modules/mu/providers/aws/dnszone.rb, line 631
        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