module Zonify
Constants
- EC2_DNS_RE
- ELB_DNS_RE
- LDH_RE
- RRTYPE_RE
Based on reading the Wikipedia page:
http://en.wikipedia.org/wiki/List_of_DNS_record_types
and the IANA registry:
http://www.iana.org/assignments/dns-parameters
Public Instance Methods
# File lib/zonify.rb, line 486 def _dot(s) /^[.]/.match(s) ? s : ".#{s}" end
The Route 53 API has limitations on query size:
- A request cannot contain more than 100 Change elements. - A request cannot contain more than 1000 ResourceRecord elements. - The sum of the number of characters (including spaces) in all Value elements in a request cannot exceed 32,000 characters.
# File lib/zonify.rb, line 547 def chunk_changesets(changes) chunks = [[]] changes.each do |change| if fits(change, chunks.last) chunks.last.push(change) else chunks.push([change]) end end chunks end
For every SRV record that is not a singleton and that does not shadow an existing CNAME, we create WRRs for item in the SRV record.
# File lib/zonify.rb, line 320 def cname_multitudinous(tree) tree.inject({}) do |acc, pair| name, info = pair name_clipped = name.sub("#{Zonify::Resolve::SRV_PREFIX}.", '') info.each do |type, data| if 'SRV' == type and 1 < data[:value].length wrrs = data[:value].inject({}) do |accumulator, rr| server = Zonify.dot_(rr.sub(/^([^ ]+ +){3}/, '').strip) id = server.split('.').first # Always the instance ID. accumulator[id] = data.merge(:value=>[server], :weight=>"16") accumulator end acc[name_clipped] = { 'CNAME' => wrrs } end end acc end end
For SRV records with a single entry, create a singleton CNAME as a convenience.
# File lib/zonify.rb, line 281 def cname_singletons(tree) tree.inject({}) do |acc, pair| name, info = pair name_clipped = name.sub("#{Zonify::Resolve::SRV_PREFIX}.", '') info.each do |type, data| if 'SRV' == type and 1 == data[:value].length rr_clipped = data[:value].map do |rr| Zonify.dot_(rr.sub(/^([^ ]+ +){3}/, '').strip) end new_data = data.merge(:value=>rr_clipped) acc[name_clipped] = { 'CNAME' => new_data } end end acc end end
Determine whether two resource record sets are the same in all respects (keys missing in one should be missing in the other).
# File lib/zonify.rb, line 433 def compare_records(a, b) keys = ((a.keys | b.keys) - [:value]).sort_by{|s| s.to_s } as, bs = [a, b].map do |record| keys.map{|k| record[k] } << Zonify.normRRs(record[:value]) end as == bs end
# File lib/zonify.rb, line 464 def cut_down_elb_name(s) $1 if ELB_DNS_RE.match(s) end
Old records that have the same elements as new records should be left as is. If they differ in any way, they should be marked for deletion and the new record marked for creation. Old records not in the new records should also be marked for deletion.
# File lib/zonify.rb, line 388 def diff(new_records, old_records, types=['CNAME','SRV']) create_set = new_records.map do |name, v| old = old_records[name] v.map do |type, data| if types.member? '*' or types.member? type old_data = ((old and old[type]) or {}) unless type == 'CNAME' and not types.member? 'A' and old and old.member? 'A' unless Zonify.compare_records(old_data, data) Zonify.hoist(data, name, type, 'CREATE') end end end end.compact end delete_set = old_records.map do |name, v| new = new_records[name] v.map do |type, data| if types.member? '*' or types.member? type new_data = ((new and new[type]) or {}) unless Zonify.compare_records(data, new_data) Zonify.hoist(data, name, type, 'DELETE') end end end.compact end (delete_set.flatten + create_set.flatten).sort_by do |record| # Sort actions so that creation of a record comes immediately after a # deletion. delete_first = record[:action] == 'DELETE' ? 0 : 1 [record[:name], record[:type], delete_first] end end
# File lib/zonify.rb, line 490 def dot_(s) /[.]$/.match(s) ? s : "#{s}." end
# File lib/zonify.rb, line 496 def ec2_dns_to_ip(dns) "#{$1}.#{$2}.#{$3}.#{$4}" if EC2_DNS_RE.match(dns) end
Determine whether we can add this record to the existing records, subject to Amazon size constraints.
# File lib/zonify.rb, line 566 def fits(change, changes) new = changes + [change] measured = new.map{|change| measureRRs(change) } len, chars = measured.inject([0, 0]) do |acc, pair| [ acc[0] + pair[0], acc[1] + pair[1] ] end new.length <= 100 and len <= 1000 and chars <= 30000 # margin of safety end
# File lib/zonify.rb, line 422 def hoist(data, name, type, action) meta = {:name=>name, :type=>type, :action=>action} if data[:value] # Not a WRR. [data.merge(meta)] else # Is a WRR. data.map{|k,v| v.merge(meta.merge(:set_identifier=>k)) } end end
# File lib/zonify.rb, line 559 def measureRRs(change) [ change[:value].length, change[:value].inject(0){|sum, s| s.length + sum } ] end
Merge all records from the trees, taking TTLs from the leftmost tree and sorting and deduplicating resource records. (When called on a single tree, this function serves to sort and deduplicate resource records.)
# File lib/zonify.rb, line 359 def merge(*trees) acc = {} trees.each do |tree| tree.inject(acc) do |acc, pair| name, info = pair acc[name] ||= {} info.inject(acc[name]) do |acc_, pair_| type, data = pair_ case when (not acc_[type]) acc_[type] = data.dup when (not acc_[type][:value] and not data[:value]) # WRR records. d = data.merge(acc_[type]) acc_[type] = d else # Not WRR records. acc_[type][:value] = (data[:value] + acc_[type][:value]).sort.uniq end acc_ end acc end end acc end
Sometimes, resource_records are a single string; sometimes, an array. The array should be sorted for comparison's sake. Strings should be put in an array.
# File lib/zonify.rb, line 444 def normRRs(val) case val when Array then val.sort else [val] end end
In the fully normalized tree of records, each multi-element SRV is associated with a set of equally weighted CNAMEs, one for each record. Singleton SRVs are associated with a single CNAME. All resource record lists are sorted and deduplicated.
# File lib/zonify.rb, line 260 def normalize(tree) singles = Zonify.cname_singletons(tree) merged = Zonify.merge(tree, singles) remove, srvs = Zonify.srv_from_cnames(merged) cleared = merged.inject({}) do |acc, pair| name, info = pair info.each do |type, data| unless 'CNAME' == type and remove.member?(name) acc[name] ||= {} acc[name][type] = data end end acc end stage2 = Zonify.merge(cleared, srvs) multis = Zonify.cname_multitudinous(stage2) stage3 = Zonify.merge(stage2, multis) end
# File lib/zonify.rb, line 451 def read_octal(s) after = s acc = '' loop do before, match, after = after.partition(/\\([0-9][0-9][0-9])/) acc += before break if match.empty? acc << $1.oct end acc end
Find CNAMEs with multiple records and create SRV records to replace them, as well as returning the list of CNAMEs to replace.
# File lib/zonify.rb, line 300 def srv_from_cnames(tree) remove = [] srvs = tree.inject({}) do |acc, pair| name, info = pair name_srv = "#{Zonify::Resolve::SRV_PREFIX}.#{name}" info.each do |type, data| if 'CNAME' == type and 1 < data[:value].length remove.push(name) rr_srv = data[:value].map{|s| '0 0 0 ' + s } acc[name_srv] ||= { } acc[name_srv]['SRV'] = { :ttl=>100, :value=>rr_srv } end end acc end [remove, srvs] end
# File lib/zonify.rb, line 475 def string_to_ldh(s) head, *tail = s.split('.') tail_ = tail.map{|s| string_to_ldh_component(s) } head_ = case head when '*' then '*' when nil then '' else string_to_ldh_component(head) end [head_, tail_].flatten.select{|c| not (c.empty? or c.nil?) }.join('.') end
# File lib/zonify.rb, line 469 def string_to_ldh_component(s) munged = LDH_RE.match(s) ? s.downcase : s.downcase.gsub(/[^a-z0-9-]/, '-'). sub(/(^[-]+|[-]+$)/, '') munged[0...63] end
Group DNS entries into a tree, with name at the top level, type at the next level and then resource records and TTL at the leaves. If the records are part of a weighted record set, then the record data is pushed down one more level, with the “set identifier” in between the type and data.
# File lib/zonify.rb, line 239 def tree(records) records.inject({}) do |acc, record| name, type, ttl, value, weight, set = [ record[:name], record[:type], record[:ttl], record[:value], record[:weight], record[:set_identifier] ] reference = acc[name] ||= {} reference = reference[type] ||= {} reference = reference[set] ||= {} if set appended = (reference[:value] or []) << value reference[:ttl] = ttl reference[:value] = appended.sort.uniq reference[:weight] = weight if weight acc end end
Collate RightAWS style records in to the tree format used by the tree method.
# File lib/zonify.rb, line 340 def tree_from_right_aws(records) records.inject({}) do |acc, record| name, type, ttl, value, weight, set = [ record[:name], record[:type], record[:ttl], record[:value], record[:weight], record[:set_identifier] ] reference = acc[name] ||= {} reference = reference[type] ||= {} reference = reference[set] ||= {} if set reference[:ttl] = ttl reference[:value] = (value or []) reference[:weight] = weight if weight acc end end
Given EC2 host and ELB data, construct unqualified DNS entries to make a zone, of sorts.
# File lib/zonify.rb, line 201 def zone(hosts, elbs) host_records = hosts.map do |id,info| name = "#{id}.inst." priv = "#{info[:priv]}.priv." [ Zonify::RR.cname(name, info[:dns], '600'), Zonify::RR.cname(priv, info[:dns], '600'), Zonify::RR.srv('inst.', name) ] + info[:tags].map do |tag| k, v = tag next if k.nil? or v.nil? or k.empty? or v.empty? tag_dn = "#{Zonify.string_to_ldh(v)}.#{Zonify.string_to_ldh(k)}.tag." Zonify::RR.srv(tag_dn, name) end.compact end.flatten elb_records = elbs.map do |elb| running = elb[:instances].select{|i| hosts[i] } name = "#{elb[:prefix]}.elb." running.map{|host| Zonify::RR.srv(name, "#{host}.inst.") } end.flatten sg_records = hosts.inject({}) do |acc, kv| id, info = kv info[:sg].each do |sg| acc[sg] ||= [] acc[sg] << id end acc end.map do |sg, ids| sg_ldh = Zonify.string_to_ldh(sg) name = "#{sg_ldh}.sg." ids.map{|id| Zonify::RR.srv(name, "#{id}.inst.") } end.flatten [host_records, elb_records, sg_records].flatten end