# 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", @config['region']) if !@config['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: @config["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: @config["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: @config["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]: 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]: 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"] = @config['region'] if !@config['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]: 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]: 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