zonify 0.4.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|