zonify 0.4.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README +165 -0
- data/bin/zonify +254 -0
- data/lib/zonify.rb +582 -0
- metadata +85 -0
data/README
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
SYNOPSIS
|
2
|
+
zonify ... (-h|-[?]|--help) ...
|
3
|
+
zonify ec2 <rewrite rules>* > zone.ec2.yaml
|
4
|
+
zonify ec2/r53 <domain> <rewrite rules>* > changes.yaml
|
5
|
+
zonify r53 <domain> > zone.r53.yaml
|
6
|
+
zonify diff zone.r53.yaml zone.ec2.yaml > changes.yaml
|
7
|
+
zonify rewrite <rewrite rules>* < zone.ec2.yaml
|
8
|
+
zonify summarize < changes.yaml
|
9
|
+
zonify apply < changes.yaml
|
10
|
+
zonify sync <domain> <rewrite rules>*
|
11
|
+
zonify normalize <domain>
|
12
|
+
zonify eips
|
13
|
+
|
14
|
+
DESCRIPTION
|
15
|
+
The zonify tool allows one to create DNS entries for all instances,
|
16
|
+
tags and load balancers in EC2 and synchronize a Route 53 zone with
|
17
|
+
these entries.
|
18
|
+
|
19
|
+
The zonify tool and libraries intelligently insert a final and initial
|
20
|
+
'.' as needed to conform to DNS conventions. One may enter the domains
|
21
|
+
at the command line as example.com or example.com.; it will work either
|
22
|
+
way.
|
23
|
+
|
24
|
+
For access to AWS APIs, zonify uses the the conventional environment
|
25
|
+
variables to select regions and specify credentials:
|
26
|
+
|
27
|
+
AWS_ACCESS_KEY AWS_ACCESS_KEY_ID
|
28
|
+
AWS_SECRET_KEY AWS_SECRET_ACCESS_KEY
|
29
|
+
EC2_URL
|
30
|
+
|
31
|
+
These variables are used by many AWS libraries and tools. As a conve-
|
32
|
+
nience, the environment variable AWS_REGION may be used with region
|
33
|
+
nicknames:
|
34
|
+
|
35
|
+
AWS_REGION=eu-west-1
|
36
|
+
|
37
|
+
The Zonify subcommands allow staged generation, transformation and
|
38
|
+
auditing of entries as well as straightforward, one-step synchroniza-
|
39
|
+
tion.
|
40
|
+
|
41
|
+
ec2 (--srv-singleton|--no-srv-singleton)?
|
42
|
+
Organizes instances, load balancers, security groups and
|
43
|
+
instance metadata into DNS entries, with the generic suffix
|
44
|
+
'.' (intended to be transformed by later commands).
|
45
|
+
|
46
|
+
ec2/r53 (--types CNAME,SRV)? (--srv-singleton|--no-srv-singleton)?
|
47
|
+
Creates a changes file, describing how records under the
|
48
|
+
given suffix would be created and deleted to bring it in to
|
49
|
+
sync with EC2. By default, only records of type CNAME and SRV
|
50
|
+
are examined and changed.
|
51
|
+
|
52
|
+
r53 Capture all Route 53 records under the given suffix.
|
53
|
+
|
54
|
+
diff (--types CNAME,SRV,A,MX,...)?
|
55
|
+
Describe changes (which can be fed to the apply subcommand)
|
56
|
+
needed to bring a Route 53 domain in the first file into sync
|
57
|
+
with domain described in the second file. The suffix is taken
|
58
|
+
from the first file. The default with diff (unlike other
|
59
|
+
zonify subcommands) is to examine all record types.
|
60
|
+
|
61
|
+
rewrite (--srv-singleton|--no-srv-singleton)?
|
62
|
+
Apply rewrite rules to the domain file.
|
63
|
+
|
64
|
+
summarize
|
65
|
+
Summarize changes in a changes file, writing to STDOUT.
|
66
|
+
|
67
|
+
apply Apply a changes file.
|
68
|
+
|
69
|
+
sync (--types CNAME,SRV)? (--srv-singleton|--no-srv-singleton)?
|
70
|
+
Sync the given domain with EC2. By default, only records of
|
71
|
+
type CNAME and SRV are examined and changed.
|
72
|
+
|
73
|
+
normalize
|
74
|
+
Create CNAMEs for SRV records that have only one server in
|
75
|
+
them and rebase records on to the given domain.
|
76
|
+
|
77
|
+
eips List all Elastic IPs and DNS entries that map to them.
|
78
|
+
|
79
|
+
The --[no-]srv-singleton options control creation of CNAMEs for single-
|
80
|
+
ton SRV records. They are enabled by default; but it can be useful to
|
81
|
+
disable them for pre-processing the YAML and then adding them with nor-
|
82
|
+
malize. For example:
|
83
|
+
|
84
|
+
The --[no-]srv-singleton options also control creation of weighted
|
85
|
+
round-robin CNAMEs, an infelicity in nomenclature.
|
86
|
+
|
87
|
+
zonify r53 amz.example.com > r53.yaml
|
88
|
+
zonify ec2 --no-srv-singleton > ec2.yaml
|
89
|
+
my-yaml-rewriter < ec2.yaml > adjusted.yaml
|
90
|
+
zonify normalize amz.example.com < adjusted.yaml > normed.yaml
|
91
|
+
zonify diff r53.yaml normed.yaml | zonify apply
|
92
|
+
|
93
|
+
SYNC POLICY
|
94
|
+
Zonify assumes the domain given on the command line is entirely under
|
95
|
+
the control of Zonify; records not reflecting the present state of EC2
|
96
|
+
are scheduled for deletion in the generated changesets. This can be
|
97
|
+
controlled to some degree with the --types option.
|
98
|
+
|
99
|
+
The sync scopes over the domain and not necessarily the entire Route 53
|
100
|
+
zone. Say, for example, one has example.com in a Route 53 zone and one
|
101
|
+
plans to use amz.example.com for Amazon instance records. In this sce-
|
102
|
+
nario, Zonify will only specify changes that delete or create records
|
103
|
+
under amz.example.com; www.example.com, s0.mobile.example.com and simi-
|
104
|
+
lar records will not be affected.
|
105
|
+
|
106
|
+
YAML OUTPUT
|
107
|
+
All records and change sets are sorted by name on output. The data com-
|
108
|
+
ponents of records are also sorted. This ensures consistent output from
|
109
|
+
run to run; and allows the diff tool to return meaningful results when
|
110
|
+
outputs are compared.
|
111
|
+
|
112
|
+
One exception to this rule is the r53 subcommand, which preserves the
|
113
|
+
order of data as it was found in Route 53.
|
114
|
+
|
115
|
+
REWRITE RULES
|
116
|
+
Rewrite rules take the form <domain>(:<domain>)+. To shorten names
|
117
|
+
under the apache security group to web.amz.example.com, use:
|
118
|
+
|
119
|
+
apache.sg:web
|
120
|
+
|
121
|
+
To keep both forms, use the rule:
|
122
|
+
|
123
|
+
apache.sg:apache.sg:web
|
124
|
+
|
125
|
+
GENERATED RECORDS AND QUERYING
|
126
|
+
For records where there are potentially many servers -- security
|
127
|
+
groups, tags, load balancers -- Zonify creates SRV records. When a SRV
|
128
|
+
record has only one entry under it, a simple CNAME is created. When a
|
129
|
+
SRV record contains multiple records, multiple weighted round-robin
|
130
|
+
CNAMEs are created, one for each server in the SRV record.
|
131
|
+
|
132
|
+
Records created include:
|
133
|
+
|
134
|
+
i-ABCD1234.inst.
|
135
|
+
Individual instances.
|
136
|
+
|
137
|
+
_*._*.<value>.<key>.tag.
|
138
|
+
SRV records for tags.
|
139
|
+
|
140
|
+
_*._*.<name>.sg.
|
141
|
+
SRV records for security groups.
|
142
|
+
|
143
|
+
_*._*.<name>.elb
|
144
|
+
SRV records for instances behind Elastic Load Balancers.
|
145
|
+
|
146
|
+
domU-*.priv., ip-*.priv
|
147
|
+
Records pointing to the default hostname, derived from the
|
148
|
+
private DNS entry, set by many AMIs.
|
149
|
+
|
150
|
+
A list of all instances is placed under inst -- continuing with our
|
151
|
+
example above, this would be the SRV record _*._*.inst.amz.example.com.
|
152
|
+
To obtain the list of all instances with dig:
|
153
|
+
|
154
|
+
dig @8.8.8.8 +tcp +short _*._*.inst.amz.example.com SRV | cut -d' ' -f4
|
155
|
+
|
156
|
+
The cut call is necessary to remove some values, always nonces with
|
157
|
+
Zonify, that are part of standard format SRV records.
|
158
|
+
|
159
|
+
EXAMPLES
|
160
|
+
# Create records under amz.example.com, with instance names appearing
|
161
|
+
# directly under .amz.example.com.
|
162
|
+
zone sync amz.example.com name.tag:.
|
163
|
+
# Similar to above but stores changes to disk for later application.
|
164
|
+
zone ec2/r53 amz.example.com name.tag:. > changes.yaml
|
165
|
+
|
data/bin/zonify
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
root = File.expand_path("#{File.dirname(__FILE__)}/..")
|
3
|
+
$LOAD_PATH.unshift("#{root}/lib") if File.directory?("#{root}/lib")
|
4
|
+
USAGE = File.read("#{root}/README")
|
5
|
+
|
6
|
+
require 'readline'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'fog'
|
11
|
+
|
12
|
+
require 'zonify'
|
13
|
+
|
14
|
+
|
15
|
+
class CLIAWS
|
16
|
+
attr_reader :options
|
17
|
+
def initialize(access=nil, secret=nil)
|
18
|
+
@options = {}
|
19
|
+
@options.merge!(options) if options
|
20
|
+
env_adapter
|
21
|
+
{ :region => ENV['AWS_REGION'],
|
22
|
+
:aws_access_key_id => (access or ENV['AWS_ACCESS_KEY']),
|
23
|
+
:aws_secret_access_key => (secret or ENV['AWS_SECRET_KEY'])
|
24
|
+
}.each{|k,v| @options.merge!(k=>v) if v }
|
25
|
+
end
|
26
|
+
def respond_to?(sym)
|
27
|
+
aws.respond_to?(sym)
|
28
|
+
end
|
29
|
+
def env_adapter
|
30
|
+
[ %w| AWS_ACCESS_KEY_ID AWS_ACCESS_KEY |,
|
31
|
+
%w| AWS_SECRET_ACCESS_KEY AWS_SECRET_KEY | ].each do |a, b|
|
32
|
+
ENV[a] = ENV[b] if ENV[b] # Ensure new variable takes precedence.
|
33
|
+
ENV[b] = ENV[a] unless ENV[b]
|
34
|
+
end
|
35
|
+
# Sometimes libraries can not not handle being passed a region if the
|
36
|
+
# EC2_URL is set.
|
37
|
+
ENV['EC2_URL'] = nil if ENV['AWS_REGION']
|
38
|
+
end
|
39
|
+
def method_missing(sym, *args, &block)
|
40
|
+
begin
|
41
|
+
aws.send(sym, *args, &block)
|
42
|
+
rescue Fog::Errors::Error => e
|
43
|
+
abort "AWS error: #{e}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
def aws
|
47
|
+
@aws ||= Zonify::AWS.create(@options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def rejected(rejected_changes)
|
52
|
+
unless rejected_changes.empty?
|
53
|
+
STDERR.puts 'Rejected some changes, because they were too large.'
|
54
|
+
rejected_changes.each do |change|
|
55
|
+
STDOUT.puts <<YAML
|
56
|
+
- :name: #{change[:name]}
|
57
|
+
:type: #{change[:type]}
|
58
|
+
YAML
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def check_name(suffix)
|
64
|
+
abort 'No domain given.' unless suffix
|
65
|
+
unless suffix.split('.').all?{|s| s.empty? or Zonify::LDH_RE.match(s) }
|
66
|
+
abort "Not a conventional, LDH domain name: #{suffix}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def display(changes)
|
71
|
+
if changes.empty?
|
72
|
+
'No changes; nothing to do.'
|
73
|
+
else
|
74
|
+
summary = changes.inject({}) do |acc, change|
|
75
|
+
name, type, set = [change[:name], change[:type], change[:set_identifier]]
|
76
|
+
key = "#{name} #{type}" + ( set ? " (#{set})" : "" )
|
77
|
+
acc[key] ||= []
|
78
|
+
acc[key] << change[:action]
|
79
|
+
acc
|
80
|
+
end.map do |k, v|
|
81
|
+
case v
|
82
|
+
when ['DELETE', 'CREATE'] then [k, 'replace']
|
83
|
+
when ['DELETE'] then [k, 'delete']
|
84
|
+
when ['CREATE'] then [k, 'create']
|
85
|
+
end
|
86
|
+
end.sort
|
87
|
+
len = summary.map{|name, op| [name.length, 64].min }.max
|
88
|
+
formatted = summary.map{|name, op| name.ljust(len) + " ---> " + op }
|
89
|
+
formatted.unshift("There are #{summary.length} changes.").join("\n")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def yaml_with_default(source, default)
|
94
|
+
s = source.read.rstrip
|
95
|
+
unless s.empty?
|
96
|
+
yaml = YAML.load(s)
|
97
|
+
abort 'Bad YAML parse.' unless yaml
|
98
|
+
yaml
|
99
|
+
else
|
100
|
+
default
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def normed_with_mappings(mapping_args, suffix, records, norm=true)
|
105
|
+
mappings = mapping_args.map{|s| Zonify::Mappings.parse(s) }
|
106
|
+
abort 'Bad mapping parse.' if mappings.any?{|r| r.nil? }
|
107
|
+
mappings.each{|k, v| ([k] + v).each{|s| check_name(s) } }
|
108
|
+
clipped = Zonify::Mappings.rewrite(records, [[suffix,['.']]])
|
109
|
+
mapped = Zonify::Mappings.rewrite(clipped, mappings)
|
110
|
+
restored = Zonify::Mappings.rewrite(mapped, [['.',[suffix]]])
|
111
|
+
norm ? Zonify.normalize(restored) : restored
|
112
|
+
end
|
113
|
+
|
114
|
+
def confirm_dialogue
|
115
|
+
# Restore terminal state on SIGINT. Needed due to use of Readline.
|
116
|
+
stty_save = `stty -g 2>/dev/null`.chomp
|
117
|
+
trap('INT') do
|
118
|
+
system('stty', stty_save)
|
119
|
+
exit
|
120
|
+
end
|
121
|
+
perform = false
|
122
|
+
msg = 'Perform changes?'
|
123
|
+
while line = Readline.readline("#{msg} [Y/n] ")
|
124
|
+
case line
|
125
|
+
when /^(n|no)$/i
|
126
|
+
perform = false
|
127
|
+
STDERR.puts 'Abandoning changes...'
|
128
|
+
break
|
129
|
+
when /^(y|yes|)$/i
|
130
|
+
perform = true
|
131
|
+
STDERR.puts 'Initiating changes...'
|
132
|
+
break
|
133
|
+
when nil
|
134
|
+
perform = false
|
135
|
+
break
|
136
|
+
else
|
137
|
+
msg = "Perform changes? Please answer with 'y' or 'n'."
|
138
|
+
end
|
139
|
+
end
|
140
|
+
perform
|
141
|
+
end
|
142
|
+
|
143
|
+
def main
|
144
|
+
if ARGV.any?{|arg| %w| help -h --help -? |.member?(arg) }
|
145
|
+
puts USAGE
|
146
|
+
exit
|
147
|
+
end
|
148
|
+
#confirm = [ARGV.delete("-c"), ARGV.delete("--confirm")].any?
|
149
|
+
confirm = false
|
150
|
+
quiet = (ARGV.delete("-q") or ARGV.delete("--quiet"))
|
151
|
+
netlogging = (ARGV.delete("-n") or ARGV.delete("--net"))
|
152
|
+
norm = (ARGV.delete("--srv-singleton") or not
|
153
|
+
ARGV.delete("--no-srv-singleton"))
|
154
|
+
aws = CLIAWS.new
|
155
|
+
types = nil
|
156
|
+
while i = ARGV.index('--types')
|
157
|
+
ARGV.delete_at(i)
|
158
|
+
types = ARGV[i].split(/ +| *, */)
|
159
|
+
ARGV.delete_at(i)
|
160
|
+
bad = types.map{|s| s if not Zonify::RRTYPE_RE.match(s) }.compact
|
161
|
+
abort "Bad RR types: #{bad.join(' ')}" unless bad.empty?
|
162
|
+
end
|
163
|
+
case ARGV[0]
|
164
|
+
when 'ec2'
|
165
|
+
data = aws.ec2_zone
|
166
|
+
mappings = (ARGV[1..-1] or [])
|
167
|
+
mapped = normed_with_mappings(mappings, '.', data, norm)
|
168
|
+
STDOUT.write Zonify::YAML.format(mapped, '.')
|
169
|
+
when 'r53'
|
170
|
+
suffix = ARGV[1]
|
171
|
+
check_name(suffix)
|
172
|
+
zone, data = aws.route53_zone(suffix)
|
173
|
+
if zone and data
|
174
|
+
STDOUT.write Zonify::YAML.format(data, suffix)
|
175
|
+
else
|
176
|
+
STDERR.puts 'No zone found; outputting nonce zone.'
|
177
|
+
STDOUT.write Zonify::YAML.format({}, '.')
|
178
|
+
end
|
179
|
+
when 'eips'
|
180
|
+
result = aws.eip_scan
|
181
|
+
entries = result.keys.sort.map do |k|
|
182
|
+
dumped = ::YAML.dump(k=>result[k])
|
183
|
+
Zonify::YAML.trim_lines(dumped).join.sub(/ *\[\]$/,'')
|
184
|
+
end.join
|
185
|
+
STDOUT.write entries
|
186
|
+
when 'diff'
|
187
|
+
suffix, old_records = Zonify::YAML.read(File.read(ARGV[1]))
|
188
|
+
new_suffix, new_records = Zonify::YAML.read(File.read(ARGV[2]))
|
189
|
+
qualified = Zonify::Mappings.rewrite(new_records, [[new_suffix,[suffix]]])
|
190
|
+
changes = Zonify.diff(qualified, old_records, (types or %w| * |))
|
191
|
+
STDERR.puts(display(changes)) unless quiet
|
192
|
+
STDOUT.write(Zonify::YAML.trim_lines(YAML.dump(changes)))
|
193
|
+
when 'sync'
|
194
|
+
suffix = ARGV[1]
|
195
|
+
check_name(suffix)
|
196
|
+
new_records = aws.ec2_zone
|
197
|
+
mappings = (ARGV[2..-1] or [])
|
198
|
+
mapped = normed_with_mappings(mappings, suffix, new_records)
|
199
|
+
_, old_records = aws.route53_zone(suffix)
|
200
|
+
changes = Zonify.diff(mapped, old_records, (types or %w| CNAME SRV |))
|
201
|
+
STDERR.puts(display(changes)) if confirm or not quiet
|
202
|
+
perform = if confirm and not changes.empty?
|
203
|
+
confirm_dialogue
|
204
|
+
else
|
205
|
+
true
|
206
|
+
end
|
207
|
+
rejected(aws.apply(changes)) if perform
|
208
|
+
when 'ec2/r53'
|
209
|
+
suffix = ARGV[1]
|
210
|
+
check_name(suffix)
|
211
|
+
new_records = aws.ec2_zone
|
212
|
+
mappings = (ARGV[2..-1] or [])
|
213
|
+
mapped = normed_with_mappings(mappings, suffix, new_records)
|
214
|
+
_, old_records = aws.route53_zone(suffix)
|
215
|
+
changes = Zonify.diff(mapped, old_records, (types or %w| CNAME SRV |))
|
216
|
+
STDERR.puts(display(changes)) unless quiet
|
217
|
+
STDOUT.write(Zonify::YAML.trim_lines(YAML.dump(changes)))
|
218
|
+
when 'summarize'
|
219
|
+
handle = ARGV[1] ? File.open(ARGV[1]) : STDIN
|
220
|
+
changes = yaml_with_default(handle, [])
|
221
|
+
STDOUT.puts(display(changes))
|
222
|
+
when 'apply'
|
223
|
+
handle = ARGV[1] ? File.open(ARGV[1]) : STDIN
|
224
|
+
changes = yaml_with_default(handle, [])
|
225
|
+
STDERR.puts "" unless changes
|
226
|
+
STDERR.puts(display(changes)) if confirm or not quiet
|
227
|
+
perform = if confirm and not changes.empty?
|
228
|
+
confirm_dialogue
|
229
|
+
else
|
230
|
+
true
|
231
|
+
end
|
232
|
+
rejected(aws.apply(changes)) if perform
|
233
|
+
when 'rewrite'
|
234
|
+
suffix, records = Zonify::YAML.read(STDIN)
|
235
|
+
mappings = (ARGV[1..-1] or [])
|
236
|
+
abort 'No mappings given.' if mappings.empty?
|
237
|
+
mapped = normed_with_mappings(mappings, suffix, records, norm)
|
238
|
+
STDOUT.write(Zonify::YAML.format(mapped, suffix))
|
239
|
+
when 'normalize'
|
240
|
+
suffix, records = Zonify::YAML.read(STDIN)
|
241
|
+
desired_suffix = ARGV[1]
|
242
|
+
normed = if desired_suffix
|
243
|
+
normed_with_mappings([], desired_suffix, records)
|
244
|
+
else
|
245
|
+
Zonify.normalize(records)
|
246
|
+
end
|
247
|
+
STDOUT.write(Zonify::YAML.format(normed, (desired_suffix or suffix)))
|
248
|
+
else
|
249
|
+
abort 'Argument error.'
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
main
|
254
|
+
|
data/lib/zonify.rb
ADDED
@@ -0,0 +1,582 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
require 'fog'
|
4
|
+
|
5
|
+
|
6
|
+
module Zonify
|
7
|
+
|
8
|
+
# Set up for AWS interfaces and access to EC2 instance metadata.
|
9
|
+
class AWS
|
10
|
+
class << self
|
11
|
+
# Retrieve the EC2 instance ID of this instance.
|
12
|
+
def local_instance_id
|
13
|
+
s = `curl -s http://169.254.169.254/latest/meta-data/instance-id`
|
14
|
+
s.strip if $?.success?
|
15
|
+
end
|
16
|
+
# Initialize all AWS interfaces with the same access keys and logger
|
17
|
+
# (probably what you want to do). These are set up lazily; unused
|
18
|
+
# interfaces will not be initialized.
|
19
|
+
def create(options)
|
20
|
+
options_ec2 = options.merge( :provider=>'AWS',
|
21
|
+
:connection_options=>{:nonblock=>false} )
|
22
|
+
ec2 = Proc.new{|| Fog::Compute.new(options_ec2) }
|
23
|
+
options_elb = options_ec2.dup.delete_if{|k, _| k == :provider }
|
24
|
+
elb = Proc.new{|| Fog::AWS::ELB.new(options_elb) }
|
25
|
+
options_r53 = options_ec2.dup.delete_if{|k, _| k == :region }
|
26
|
+
r53 = Proc.new{|| Fog::DNS.new(options_r53) }
|
27
|
+
Zonify::AWS.new(:ec2_proc=>ec2, :elb_proc=>elb, :r53_proc=>r53)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
attr_reader :ec2_proc, :elb_proc, :r53_proc
|
31
|
+
def initialize(opts={})
|
32
|
+
@ec2 = opts[:ec2]
|
33
|
+
@elb = opts[:elb]
|
34
|
+
@r53 = opts[:r53]
|
35
|
+
@ec2_proc = opts[:ec2_proc]
|
36
|
+
@elb_proc = opts[:elb_proc]
|
37
|
+
@r53_proc = opts[:r53_proc]
|
38
|
+
end
|
39
|
+
def ec2
|
40
|
+
@ec2 ||= @ec2_proc.call
|
41
|
+
end
|
42
|
+
def elb
|
43
|
+
@elb ||= @elb_proc.call
|
44
|
+
end
|
45
|
+
def r53
|
46
|
+
@r53 ||= @r53_proc.call
|
47
|
+
end
|
48
|
+
# Generate DNS entries based on EC2 instances, security groups and ELB load
|
49
|
+
# balancers under the user's AWS account.
|
50
|
+
def ec2_zone
|
51
|
+
Zonify.tree(Zonify.zone(instances, load_balancers))
|
52
|
+
end
|
53
|
+
# Retrieve Route53 zone data -- the zone ID as well as resource records --
|
54
|
+
# relevant to the given suffix. When there is any ambiguity, the zone with
|
55
|
+
# the longest name is chosen.
|
56
|
+
def route53_zone(suffix)
|
57
|
+
suffix_ = Zonify.dot_(suffix)
|
58
|
+
relevant_zone = r53.zones.select do |zone|
|
59
|
+
suffix_.end_with?(zone.domain)
|
60
|
+
end.sort_by{|zone| zone.domain.length }.last
|
61
|
+
if relevant_zone
|
62
|
+
relevant_records = relevant_zone.records.all!.map do |rr|
|
63
|
+
if rr.name.end_with?(suffix_)
|
64
|
+
rr.attributes.merge(:name=>Zonify.read_octal(rr.name))
|
65
|
+
end
|
66
|
+
end.compact
|
67
|
+
[relevant_zone, Zonify.tree_from_right_aws(relevant_records)]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
# Apply a changeset to the records in Route53. The records must all be under
|
71
|
+
# the same zone and suffix.
|
72
|
+
def apply(changes, comment='Synced with Zonify tool.')
|
73
|
+
# Dumb way to do this because I can not figure out #reject!
|
74
|
+
keep = changes.select{|c| c[:value].length <= 100 }
|
75
|
+
filtered = changes.select{|c| c[:value].length > 100 }
|
76
|
+
unless keep.empty?
|
77
|
+
suffix = keep.first[:name] # Works because of longest submatch rule.
|
78
|
+
zone, _ = route53_zone(suffix)
|
79
|
+
Zonify.chunk_changesets(keep).each do |changeset|
|
80
|
+
rekeyed = changeset.map do |record|
|
81
|
+
record.inject({}) do |acc, pair|
|
82
|
+
k, v = pair
|
83
|
+
k_ = k == :value ? :resource_records : k
|
84
|
+
acc[k_] = v
|
85
|
+
acc
|
86
|
+
end
|
87
|
+
end
|
88
|
+
r53.change_resource_record_sets(zone.id, rekeyed, :comment=>comment)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
filtered
|
92
|
+
end
|
93
|
+
def instances
|
94
|
+
ec2.servers.inject({}) do |acc, i|
|
95
|
+
dns = i.dns_name
|
96
|
+
# The default hostname for EC2 instances is derived from their internal
|
97
|
+
# DNS entry.
|
98
|
+
unless dns.nil? or dns.empty?
|
99
|
+
groups = (i.groups or [])
|
100
|
+
attrs = { :sg => groups,
|
101
|
+
:tags => (i.tags or []),
|
102
|
+
:dns => Zonify.dot_(dns).downcase }
|
103
|
+
if i.private_dns_name
|
104
|
+
attrs[:priv] = i.private_dns_name.split('.').first.downcase
|
105
|
+
end
|
106
|
+
acc[i.id] = attrs
|
107
|
+
end
|
108
|
+
acc
|
109
|
+
end
|
110
|
+
end
|
111
|
+
def load_balancers
|
112
|
+
elb.load_balancers.map do |elb|
|
113
|
+
{ :instances => elb.instances,
|
114
|
+
:prefix => Zonify.cut_down_elb_name(elb.dns_name) }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
def eips
|
118
|
+
ec2.addresses
|
119
|
+
end
|
120
|
+
def eip_scan
|
121
|
+
addresses = eips.map{|eip| eip.public_ip }
|
122
|
+
result = {}
|
123
|
+
addresses.each{|a| result[a] = [] }
|
124
|
+
r53.zones.sort_by{|zone| zone.domain.reverse }.each do |zone|
|
125
|
+
zone.records.all!.each do |rr|
|
126
|
+
check = case rr.type
|
127
|
+
when 'CNAME'
|
128
|
+
rr.value.map do |s|
|
129
|
+
Zonify.ec2_dns_to_ip(s)
|
130
|
+
end.compact
|
131
|
+
when 'A','AAAA'
|
132
|
+
rr.value
|
133
|
+
end
|
134
|
+
check ||= []
|
135
|
+
found = addresses.select{|a| check.member? a }.sort
|
136
|
+
unless found.empty?
|
137
|
+
name = Zonify.read_octal(rr.name)
|
138
|
+
found.each{|a| result[a] << name }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
result
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
extend self
|
147
|
+
|
148
|
+
|
149
|
+
module Resolve
|
150
|
+
SRV_PREFIX = '_*._*'
|
151
|
+
end
|
152
|
+
|
153
|
+
# Records are all created with functions in this module, which ensures the
|
154
|
+
# necessary SRV prefixes, uniform TTLs and final dots in names.
|
155
|
+
module RR
|
156
|
+
extend self
|
157
|
+
def srv(service, name)
|
158
|
+
{ :type=>'SRV', :value=>"0 0 0 #{Zonify.dot_(name)}",
|
159
|
+
:ttl=>'100', :name=>"#{Zonify::Resolve::SRV_PREFIX}.#{service}" }
|
160
|
+
end
|
161
|
+
def cname(name, dns, ttl='100')
|
162
|
+
{ :type=>'CNAME', :value=>Zonify.dot_(dns),
|
163
|
+
:ttl=>ttl, :name=>Zonify.dot_(name) }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Given EC2 host and ELB data, construct unqualified DNS entries to make a
|
168
|
+
# zone, of sorts.
|
169
|
+
def zone(hosts, elbs)
|
170
|
+
host_records = hosts.map do |id,info|
|
171
|
+
name = "#{id}.inst."
|
172
|
+
priv = "#{info[:priv]}.priv."
|
173
|
+
[ Zonify::RR.cname(name, info[:dns], '86400'),
|
174
|
+
Zonify::RR.cname(priv, info[:dns], '86400'),
|
175
|
+
Zonify::RR.srv('inst.', name) ] +
|
176
|
+
info[:tags].map do |tag|
|
177
|
+
k, v = tag
|
178
|
+
next if k.nil? or v.nil? or k.empty? or v.empty?
|
179
|
+
tag_dn = "#{Zonify.string_to_ldh(v)}.#{Zonify.string_to_ldh(k)}.tag."
|
180
|
+
Zonify::RR.srv(tag_dn, name)
|
181
|
+
end.compact
|
182
|
+
end.flatten
|
183
|
+
elb_records = elbs.map do |elb|
|
184
|
+
running = elb[:instances].select{|i| hosts[i] }
|
185
|
+
name = "#{elb[:prefix]}.elb."
|
186
|
+
running.map{|host| Zonify::RR.srv(name, "#{host}.inst.") }
|
187
|
+
end.flatten
|
188
|
+
sg_records = hosts.inject({}) do |acc, kv|
|
189
|
+
id, info = kv
|
190
|
+
info[:sg].each do |sg|
|
191
|
+
acc[sg] ||= []
|
192
|
+
acc[sg] << id
|
193
|
+
end
|
194
|
+
acc
|
195
|
+
end.map do |sg, ids|
|
196
|
+
sg_ldh = Zonify.string_to_ldh(sg)
|
197
|
+
name = "#{sg_ldh}.sg."
|
198
|
+
ids.map{|id| Zonify::RR.srv(name, "#{id}.inst.") }
|
199
|
+
end.flatten
|
200
|
+
[host_records, elb_records, sg_records].flatten
|
201
|
+
end
|
202
|
+
|
203
|
+
# Group DNS entries into a tree, with name at the top level, type at the
|
204
|
+
# next level and then resource records and TTL at the leaves. If the records
|
205
|
+
# are part of a weighted record set, then the record data is pushed down one
|
206
|
+
# more level, with the "set identifier" in between the type and data.
|
207
|
+
def tree(records)
|
208
|
+
records.inject({}) do |acc, record|
|
209
|
+
name, type, ttl, value,
|
210
|
+
weight, set = [ record[:name], record[:type],
|
211
|
+
record[:ttl], record[:value],
|
212
|
+
record[:weight], record[:set_identifier] ]
|
213
|
+
reference = acc[name] ||= {}
|
214
|
+
reference = reference[type] ||= {}
|
215
|
+
reference = reference[set] ||= {} if set
|
216
|
+
appended = (reference[:value] or []) << value
|
217
|
+
reference[:ttl] = ttl
|
218
|
+
reference[:value] = appended.sort.uniq
|
219
|
+
reference[:weight] = weight if weight
|
220
|
+
acc
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# In the fully normalized tree of records, each multi-element SRV is
|
225
|
+
# associated with a set of equally weighted CNAMEs, one for each record.
|
226
|
+
# Singleton SRVs are associated with a single CNAME. All resource record lists
|
227
|
+
# are sorted and deduplicated.
|
228
|
+
def normalize(tree)
|
229
|
+
singles = Zonify.cname_singletons(tree)
|
230
|
+
merged = Zonify.merge(tree, singles)
|
231
|
+
remove, srvs = Zonify.srv_from_cnames(merged)
|
232
|
+
cleared = merged.inject({}) do |acc, pair|
|
233
|
+
name, info = pair
|
234
|
+
info.each do |type, data|
|
235
|
+
unless 'CNAME' == type and remove.member?(name)
|
236
|
+
acc[name] ||= {}
|
237
|
+
acc[name][type] = data
|
238
|
+
end
|
239
|
+
end
|
240
|
+
acc
|
241
|
+
end
|
242
|
+
stage2 = Zonify.merge(cleared, srvs)
|
243
|
+
multis = Zonify.cname_multitudinous(stage2)
|
244
|
+
stage3 = Zonify.merge(stage2, multis)
|
245
|
+
end
|
246
|
+
|
247
|
+
# For SRV records with a single entry, create a singleton CNAME as a
|
248
|
+
# convenience.
|
249
|
+
def cname_singletons(tree)
|
250
|
+
tree.inject({}) do |acc, pair|
|
251
|
+
name, info = pair
|
252
|
+
name_clipped = name.sub("#{Zonify::Resolve::SRV_PREFIX}.", '')
|
253
|
+
info.each do |type, data|
|
254
|
+
if 'SRV' == type and 1 == data[:value].length
|
255
|
+
rr_clipped = data[:value].map do |rr|
|
256
|
+
Zonify.dot_(rr.sub(/^([^ ]+ +){3}/, '').strip)
|
257
|
+
end
|
258
|
+
new_data = data.merge(:value=>rr_clipped)
|
259
|
+
acc[name_clipped] = { 'CNAME' => new_data }
|
260
|
+
end
|
261
|
+
end
|
262
|
+
acc
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Find CNAMEs with multiple records and create SRV records to replace them,
|
267
|
+
# as well as returning the list of CNAMEs to replace.
|
268
|
+
def srv_from_cnames(tree)
|
269
|
+
remove = []
|
270
|
+
srvs = tree.inject({}) do |acc, pair|
|
271
|
+
name, info = pair
|
272
|
+
name_srv = "#{Zonify::Resolve::SRV_PREFIX}.#{name}"
|
273
|
+
info.each do |type, data|
|
274
|
+
if 'CNAME' == type and 1 < data[:value].length
|
275
|
+
remove.push(name)
|
276
|
+
rr_srv = data[:value].map{|s| '0 0 0 ' + s }
|
277
|
+
acc[name_srv] ||= { }
|
278
|
+
acc[name_srv]['SRV'] = { :ttl=>100, :value=>rr_srv }
|
279
|
+
end
|
280
|
+
end
|
281
|
+
acc
|
282
|
+
end
|
283
|
+
[remove, srvs]
|
284
|
+
end
|
285
|
+
|
286
|
+
# For every SRV record that is not a singleton and that does not shadow an
|
287
|
+
# existing CNAME, we create WRRs for item in the SRV record.
|
288
|
+
def cname_multitudinous(tree)
|
289
|
+
tree.inject({}) do |acc, pair|
|
290
|
+
name, info = pair
|
291
|
+
name_clipped = name.sub("#{Zonify::Resolve::SRV_PREFIX}.", '')
|
292
|
+
info.each do |type, data|
|
293
|
+
if 'SRV' == type and 1 < data[:value].length
|
294
|
+
wrrs = data[:value].inject({}) do |accumulator, rr|
|
295
|
+
server = Zonify.dot_(rr.sub(/^([^ ]+ +){3}/, '').strip)
|
296
|
+
id = server.split('.').first # Always the isntance ID.
|
297
|
+
accumulator[id] = data.merge(:value=>[server], :weight=>"16")
|
298
|
+
accumulator
|
299
|
+
end
|
300
|
+
acc[name_clipped] = { 'CNAME' => wrrs }
|
301
|
+
end
|
302
|
+
end
|
303
|
+
acc
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Collate RightAWS style records in to the tree format used by the tree method.
|
308
|
+
def tree_from_right_aws(records)
|
309
|
+
records.inject({}) do |acc, record|
|
310
|
+
name, type, ttl, value,
|
311
|
+
weight, set = [ record[:name], record[:type],
|
312
|
+
record[:ttl], record[:value],
|
313
|
+
record[:weight], record[:set_identifier] ]
|
314
|
+
reference = acc[name] ||= {}
|
315
|
+
reference = reference[type] ||= {}
|
316
|
+
reference = reference[set] ||= {} if set
|
317
|
+
reference[:ttl] = ttl
|
318
|
+
reference[:value] = (value or [])
|
319
|
+
reference[:weight] = weight if weight
|
320
|
+
acc
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Merge all records from the trees, taking TTLs from the leftmost tree and
|
325
|
+
# sorting and deduplicating resource records. (When called on a single tree,
|
326
|
+
# this function serves to sort and deduplicate resource records.)
|
327
|
+
def merge(*trees)
|
328
|
+
acc = {}
|
329
|
+
trees.each do |tree|
|
330
|
+
tree.inject(acc) do |acc, pair|
|
331
|
+
name, info = pair
|
332
|
+
acc[name] ||= {}
|
333
|
+
info.inject(acc[name]) do |acc_, pair_|
|
334
|
+
type, data = pair_
|
335
|
+
case
|
336
|
+
when (not acc_[type])
|
337
|
+
acc_[type] = data.dup
|
338
|
+
when (not acc_[type][:value] and not data[:value]) # WRR records.
|
339
|
+
d = data.merge(acc_[type])
|
340
|
+
acc_[type] = d
|
341
|
+
else # Not WRR records.
|
342
|
+
acc_[type][:value] = (data[:value] + acc_[type][:value]).uniq.sort
|
343
|
+
end
|
344
|
+
acc_
|
345
|
+
end
|
346
|
+
acc
|
347
|
+
end
|
348
|
+
end
|
349
|
+
acc
|
350
|
+
end
|
351
|
+
|
352
|
+
# Old records that have the same elements as new records should be left as is.
|
353
|
+
# If they differ in any way, they should be marked for deletion and the new
|
354
|
+
# record marked for creation. Old records not in the new records should also
|
355
|
+
# be marked for deletion.
|
356
|
+
def diff(new_records, old_records, types=['CNAME','SRV'])
|
357
|
+
create_set = new_records.map do |name, v|
|
358
|
+
old = old_records[name]
|
359
|
+
v.map do |type, data|
|
360
|
+
if types.member? '*' or types.member? type
|
361
|
+
old_data = ((old and old[type]) or {})
|
362
|
+
unless Zonify.compare_records(old_data, data)
|
363
|
+
Zonify.hoist(data, name, type, 'CREATE')
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end.compact
|
367
|
+
end
|
368
|
+
delete_set = old_records.map do |name, v|
|
369
|
+
new = new_records[name]
|
370
|
+
v.map do |type, data|
|
371
|
+
if types.member? '*' or types.member? type
|
372
|
+
new_data = ((new and new[type]) or {})
|
373
|
+
unless Zonify.compare_records(data, new_data)
|
374
|
+
Zonify.hoist(data, name, type, 'DELETE')
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end.compact
|
378
|
+
end
|
379
|
+
(delete_set.flatten + create_set.flatten).sort_by do |record|
|
380
|
+
# Sort actions so that creation of a record comes immediately after a
|
381
|
+
# deletion.
|
382
|
+
delete_first = record[:action] == 'DELETE' ? 0 : 1
|
383
|
+
[record[:name], record[:type], delete_first]
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def hoist(data, name, type, action)
|
388
|
+
meta = {:name=>name, :type=>type, :action=>action}
|
389
|
+
if data[:value] # Not a WRR.
|
390
|
+
[data.merge(meta)]
|
391
|
+
else # Is a WRR.
|
392
|
+
data.map{|k,v| v.merge(meta.merge(:set_identifier=>k)) }
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# Determine whether two resource record sets are the same in all respects
|
397
|
+
# (keys missing in one should be missing in the other).
|
398
|
+
def compare_records(a, b)
|
399
|
+
keys = ((a.keys | b.keys) - [:value]).sort_by{|s| s.to_s }
|
400
|
+
as, bs = [a, b].map do |record|
|
401
|
+
keys.map{|k| record[k] } << Zonify.normRRs(record[:value])
|
402
|
+
end
|
403
|
+
as == bs
|
404
|
+
end
|
405
|
+
|
406
|
+
# Sometimes, resource_records are a single string; sometimes, an array. The
|
407
|
+
# array should be sorted for comparison's sake. Strings should be put in an
|
408
|
+
# array.
|
409
|
+
def normRRs(val)
|
410
|
+
case val
|
411
|
+
when Array then val.sort
|
412
|
+
else [val]
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def read_octal(s)
|
417
|
+
after = s
|
418
|
+
acc = ''
|
419
|
+
loop do
|
420
|
+
before, match, after = after.partition(/\\([0-9][0-9][0-9])/)
|
421
|
+
acc += before
|
422
|
+
break if match.empty?
|
423
|
+
acc << $1.oct
|
424
|
+
end
|
425
|
+
acc
|
426
|
+
end
|
427
|
+
|
428
|
+
ELB_DNS_RE = /^([a-z0-9-]+)-[^-.]+[.].+$/
|
429
|
+
def cut_down_elb_name(s)
|
430
|
+
$1 if ELB_DNS_RE.match(s)
|
431
|
+
end
|
432
|
+
|
433
|
+
LDH_RE = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])$/
|
434
|
+
def string_to_ldh_component(s)
|
435
|
+
LDH_RE.match(s) ? s.downcase : s.downcase.gsub(/[^a-z0-9-]/, '-').
|
436
|
+
sub(/(^[-]+|[-]+$)/, '')[0,63]
|
437
|
+
end
|
438
|
+
|
439
|
+
def string_to_ldh(s)
|
440
|
+
s.split('.').map{|s| string_to_ldh_component(s) }.join('.')
|
441
|
+
end
|
442
|
+
|
443
|
+
def _dot(s)
|
444
|
+
/^[.]/.match(s) ? s : ".#{s}"
|
445
|
+
end
|
446
|
+
|
447
|
+
def dot_(s)
|
448
|
+
/[.]$/.match(s) ? s : "#{s}."
|
449
|
+
end
|
450
|
+
|
451
|
+
EC2_DNS_RE = /^ec2-([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+)
|
452
|
+
[.]compute-[0-9]+[.]amazonaws[.]com[.]?$/x
|
453
|
+
def ec2_dns_to_ip(dns)
|
454
|
+
"#{$1}.#{$2}.#{$3}.#{$4}" if EC2_DNS_RE.match(dns)
|
455
|
+
end
|
456
|
+
|
457
|
+
module YAML
|
458
|
+
extend self
|
459
|
+
def format(records, suffix='')
|
460
|
+
_suffix_ = Zonify._dot(Zonify.dot_(suffix))
|
461
|
+
entries = records.keys.sort.map do |k|
|
462
|
+
dumped = ::YAML.dump(k=>records[k])
|
463
|
+
Zonify::YAML.trim_lines(dumped).map{|ln| ' ' + ln }.join
|
464
|
+
end.join
|
465
|
+
"suffix: #{_suffix_}\nrecords:\n" + entries
|
466
|
+
end
|
467
|
+
def read(text)
|
468
|
+
yaml = ::YAML.load(text)
|
469
|
+
if yaml['suffix'] and yaml['records']
|
470
|
+
[yaml['suffix'], yaml['records']]
|
471
|
+
end
|
472
|
+
end
|
473
|
+
def trim_lines(yaml)
|
474
|
+
lines = yaml.lines.to_a
|
475
|
+
lines.shift if /^---/.match(lines[0])
|
476
|
+
lines.pop if /^$/.match(lines[-1])
|
477
|
+
lines
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
# The Route 53 API has limitations on query size:
|
482
|
+
#
|
483
|
+
# - A request cannot contain more than 100 Change elements.
|
484
|
+
#
|
485
|
+
# - A request cannot contain more than 1000 ResourceRecord elements.
|
486
|
+
#
|
487
|
+
# - The sum of the number of characters (including spaces) in all Value
|
488
|
+
# elements in a request cannot exceed 32,000 characters.
|
489
|
+
#
|
490
|
+
def chunk_changesets(changes)
|
491
|
+
chunks = [[]]
|
492
|
+
changes.each do |change|
|
493
|
+
if fits(change, chunks.last)
|
494
|
+
chunks.last.push(change)
|
495
|
+
else
|
496
|
+
chunks.push([change])
|
497
|
+
end
|
498
|
+
end
|
499
|
+
chunks
|
500
|
+
end
|
501
|
+
|
502
|
+
def measureRRs(change)
|
503
|
+
[ change[:value].length,
|
504
|
+
change[:value].inject(0){|sum, s| s.length + sum } ]
|
505
|
+
end
|
506
|
+
|
507
|
+
# Determine whether we can add this record to the existing records, subject to
|
508
|
+
# Amazon size constraints.
|
509
|
+
def fits(change, changes)
|
510
|
+
new = changes + [change]
|
511
|
+
measured = new.map{|change| measureRRs(change) }
|
512
|
+
len, chars = measured.inject([0, 0]) do |acc, pair|
|
513
|
+
[ acc[0] + pair[0], acc[1] + pair[1] ]
|
514
|
+
end
|
515
|
+
new.length <= 100 and len <= 1000 and chars <= 30000 # margin of safety
|
516
|
+
end
|
517
|
+
|
518
|
+
module Mappings
|
519
|
+
extend self
|
520
|
+
def parse(s)
|
521
|
+
k, *v = s.split(':')
|
522
|
+
[k, v] if k and v and not v.empty?
|
523
|
+
end
|
524
|
+
# Apply mappings to the name in order. (A hash can be used for mappings but
|
525
|
+
# then one will not be able to predict the order.) If no mappings apply, the
|
526
|
+
# empty list is returned.
|
527
|
+
def apply(name, mappings)
|
528
|
+
name_ = Zonify.dot_(name)
|
529
|
+
mappings.map do |k, v|
|
530
|
+
_k_ = Zonify.dot_(Zonify._dot(k))
|
531
|
+
before = Zonify::Mappings.unsuffix(name_, _k_)
|
532
|
+
v.map{|s| Zonify.dot_(before + Zonify._dot(s)) } if before
|
533
|
+
end.compact.flatten
|
534
|
+
end
|
535
|
+
# Get the names that result from the mappings, or the original name if none
|
536
|
+
# apply. The first name in the list is taken to be the canonical name, the
|
537
|
+
# one used for groups of servers in SRV records.
|
538
|
+
def names(name, mappings)
|
539
|
+
mapped = Zonify::Mappings.apply(name, mappings)
|
540
|
+
mapped.empty? ? [name] : mapped
|
541
|
+
end
|
542
|
+
def unsuffix(s, suffix)
|
543
|
+
before, _, after = s.rpartition(suffix)
|
544
|
+
before if after.empty?
|
545
|
+
end
|
546
|
+
def rewrite(tree, mappings)
|
547
|
+
tree.inject({}) do |acc, pair|
|
548
|
+
name, info = pair
|
549
|
+
names = Zonify::Mappings.names(name, mappings)
|
550
|
+
names.each do |name|
|
551
|
+
acc[name] ||= {}
|
552
|
+
info.inject(acc[name]) do |acc_, pair_|
|
553
|
+
type, data = pair_
|
554
|
+
acc_[type] ||= {}
|
555
|
+
prefix_ = Zonify.dot_(Zonify::Resolve::SRV_PREFIX)
|
556
|
+
rrs = if type == 'SRV' and name.start_with? prefix_ and data[:value]
|
557
|
+
data[:value].map do |rr|
|
558
|
+
if /^(.+) ([^ ]+)$/.match(rr)
|
559
|
+
"#{$1} #{Zonify::Mappings.names($2, mappings).first}"
|
560
|
+
else
|
561
|
+
rr
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|
565
|
+
addenda = rrs ? { :value => rrs + (acc_[type][:value] or []) } : {}
|
566
|
+
acc_[type] = data.merge(addenda)
|
567
|
+
acc_
|
568
|
+
end
|
569
|
+
end
|
570
|
+
acc
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
# Based on reading the Wikipedia page:
|
576
|
+
# http://en.wikipedia.org/wiki/List_of_DNS_record_types
|
577
|
+
# and the IANA registry:
|
578
|
+
# http://www.iana.org/assignments/dns-parameters
|
579
|
+
RRTYPE_RE = /^([*]|[A-Z0-9-]+)$/
|
580
|
+
|
581
|
+
end
|
582
|
+
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zonify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 101
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 4
|
9
|
+
- 0
|
10
|
+
- 5
|
11
|
+
version: 0.4.0.5
|
12
|
+
platform: ruby
|
13
|
+
authors:
|
14
|
+
- AirBNB
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2012-09-27 00:00:00 Z
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: fog
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
description: |
|
36
|
+
Zonify provides a command line tool for generating DNS records from EC2
|
37
|
+
instances, instance tags, load balancers and security groups. A mechanism for
|
38
|
+
syncing these records with a zone stored in Route 53 is also provided.
|
39
|
+
|
40
|
+
email: contact@airbnb.com
|
41
|
+
executables:
|
42
|
+
- zonify
|
43
|
+
extensions: []
|
44
|
+
|
45
|
+
extra_rdoc_files: []
|
46
|
+
|
47
|
+
files:
|
48
|
+
- lib/zonify.rb
|
49
|
+
- README
|
50
|
+
- bin/zonify
|
51
|
+
homepage: https://github.com/airbnb/zonify
|
52
|
+
licenses:
|
53
|
+
- BSD
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.8.24
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Generate DNS information from EC2 metadata.
|
84
|
+
test_files: []
|
85
|
+
|