roadworker 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +15 -2
- data/bin/roadwork +41 -14
- data/lib/roadworker/client.rb +20 -10
- data/lib/roadworker/dsl-tester.rb +177 -0
- data/lib/roadworker/dsl.rb +6 -0
- data/lib/roadworker/version.rb +1 -1
- metadata +19 -2
data/README.md
CHANGED
@@ -30,8 +30,8 @@ shell> export AWS_ACCESS_KEY_ID='...'
|
|
30
30
|
shell> export AWS_SECRET_ACCESS_KEY='...'
|
31
31
|
shell> roadwork -e -o Routefile
|
32
32
|
shell> vi Routefile
|
33
|
-
shell> roadwork --dry-run
|
34
|
-
shell> roudwork
|
33
|
+
shell> roadwork -a --dry-run
|
34
|
+
shell> roudwork -a
|
35
35
|
```
|
36
36
|
|
37
37
|
## Routefile example
|
@@ -72,3 +72,16 @@ hosted_zone "info.winebarrel.jp." do
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
```
|
75
|
+
|
76
|
+
## Test
|
77
|
+
|
78
|
+
Routefile compares the results of a query to the DNS and DSL in the test mode.
|
79
|
+
|
80
|
+
```
|
81
|
+
shell> roadwork -t
|
82
|
+
..F..
|
83
|
+
info.winebarrel.jp. A:
|
84
|
+
expected=127.0.0.1(300),127.0.0.3(300)
|
85
|
+
actual=127.0.0.1(300),127.0.0.2(300)
|
86
|
+
5 examples, 1 failure
|
87
|
+
```
|
data/bin/roadwork
CHANGED
@@ -7,12 +7,13 @@ require 'logger'
|
|
7
7
|
|
8
8
|
file = 'Routefile'
|
9
9
|
output_file = '-'
|
10
|
-
|
10
|
+
mode = nil
|
11
11
|
logger = Logger.new($stdout)
|
12
12
|
|
13
13
|
logger.formatter = proc {|severity, datetime, progname, msg|
|
14
14
|
"#{msg}\n"
|
15
15
|
}
|
16
|
+
|
16
17
|
options = {
|
17
18
|
:logger => logger,
|
18
19
|
:dry_run => false,
|
@@ -25,15 +26,17 @@ ARGV.options do |opt|
|
|
25
26
|
access_key = nil
|
26
27
|
secret_key = nil
|
27
28
|
|
28
|
-
opt.on('-k', '--access-key ACCESS_KEY') {|v| access_key = v
|
29
|
-
opt.on('-s', '--secret-key SECRET_KEY') {|v| secret_key = v
|
30
|
-
opt.on('-f', '--file FILE') {|v| file = v
|
31
|
-
opt.on('-
|
32
|
-
opt.on('-
|
33
|
-
opt.on('',
|
34
|
-
opt.on(''
|
35
|
-
opt.on(''
|
36
|
-
opt.on('' , '--
|
29
|
+
opt.on('-k', '--access-key ACCESS_KEY') {|v| access_key = v }
|
30
|
+
opt.on('-s', '--secret-key SECRET_KEY') {|v| secret_key = v }
|
31
|
+
opt.on('-f', '--file FILE') {|v| file = v }
|
32
|
+
opt.on('-a', '--apply') {|v| mode = :apply }
|
33
|
+
opt.on('-e', '--export') {|v| mode = :export }
|
34
|
+
opt.on('-o', '--output FILE') {|v| output_file = v }
|
35
|
+
opt.on('-t', '--test') {|v| mode = :test }
|
36
|
+
opt.on('', '--dry-run') {|v| options[:dry_run] = true }
|
37
|
+
opt.on('' , '--force') { options[:force] = true }
|
38
|
+
opt.on('' , '--no-color') { options[:color] = false }
|
39
|
+
opt.on('' , '--debug') { options[:debug] = true }
|
37
40
|
opt.parse!
|
38
41
|
|
39
42
|
if access_key and secret_key
|
@@ -41,7 +44,7 @@ ARGV.options do |opt|
|
|
41
44
|
:access_key_id => access_key,
|
42
45
|
:secret_access_key => secret_key,
|
43
46
|
})
|
44
|
-
elsif (access_key and !secret_key) or (!access_key and secret_key)
|
47
|
+
elsif (access_key and !secret_key) or (!access_key and secret_key) or mode.nil?
|
45
48
|
puts opt.help
|
46
49
|
exit 1
|
47
50
|
end
|
@@ -55,19 +58,40 @@ if options[:debug]
|
|
55
58
|
end
|
56
59
|
|
57
60
|
begin
|
61
|
+
logger = options[:logger]
|
62
|
+
logger.level = options[:debug] ? Logger::DEBUG : Logger::INFO
|
63
|
+
|
58
64
|
client = Roadworker::Client.new(options)
|
59
65
|
|
60
|
-
|
66
|
+
case mode
|
67
|
+
when :export
|
61
68
|
exported = client.export
|
62
69
|
|
63
70
|
if output_file == '-'
|
64
|
-
logger.info('Export Route53')
|
71
|
+
logger.info('# Export Route53')
|
65
72
|
puts client.export
|
66
73
|
else
|
67
74
|
logger.info("Export Route53 to `#{output_file}`")
|
68
75
|
open(output_file, 'wb') {|f| f.puts client.export }
|
69
76
|
end
|
70
|
-
|
77
|
+
when :test
|
78
|
+
# XXX:
|
79
|
+
unless File.exist?(file)
|
80
|
+
raise "No Routefile found (looking for: #{file})"
|
81
|
+
end
|
82
|
+
|
83
|
+
examples, failures = client.test(file)
|
84
|
+
examples_message = (examples > 1 ? "%d examples" : "%d example") % examples
|
85
|
+
failures_message = (failures > 1 ? "%d failures" : "%d failure") % failures
|
86
|
+
result_message = [examples_message, failures_message].join(', ')
|
87
|
+
|
88
|
+
if failures.zero?
|
89
|
+
logger.info(result_message.green)
|
90
|
+
else
|
91
|
+
logger.info(result_message.red)
|
92
|
+
exit 1
|
93
|
+
end
|
94
|
+
when :apply
|
71
95
|
unless File.exist?(file)
|
72
96
|
raise "No Routefile found (looking for: #{file})"
|
73
97
|
end
|
@@ -79,11 +103,14 @@ begin
|
|
79
103
|
updated = client.apply(file)
|
80
104
|
|
81
105
|
logger.info('No change'.intense_blue) unless updated
|
106
|
+
else
|
107
|
+
raise 'must not happen'
|
82
108
|
end
|
83
109
|
rescue => e
|
84
110
|
if options[:debug]
|
85
111
|
raise e
|
86
112
|
else
|
87
113
|
$stderr.puts e
|
114
|
+
exit 1
|
88
115
|
end
|
89
116
|
end
|
data/lib/roadworker/client.rb
CHANGED
@@ -19,16 +19,7 @@ module Roadworker
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def apply(file)
|
22
|
-
dsl =
|
23
|
-
|
24
|
-
if file.kind_of?(String)
|
25
|
-
open(file) do |f|
|
26
|
-
dsl = DSL.define(f.read, file).result
|
27
|
-
end
|
28
|
-
else
|
29
|
-
dsl = DSL.define(file.read, file.path).result
|
30
|
-
end
|
31
|
-
|
22
|
+
dsl = load_file(file)
|
32
23
|
updated = false
|
33
24
|
|
34
25
|
if dsl.hosted_zones.empty? and not @options.force
|
@@ -48,8 +39,27 @@ module Roadworker
|
|
48
39
|
DSL.convert(exported)
|
49
40
|
end
|
50
41
|
|
42
|
+
def test(file)
|
43
|
+
dsl = load_file(file)
|
44
|
+
DSL.test(dsl, @options)
|
45
|
+
end
|
46
|
+
|
51
47
|
private
|
52
48
|
|
49
|
+
def load_file(file)
|
50
|
+
dsl = nil
|
51
|
+
|
52
|
+
if file.kind_of?(String)
|
53
|
+
open(file) do |f|
|
54
|
+
dsl = DSL.define(f.read, file).result
|
55
|
+
end
|
56
|
+
else
|
57
|
+
dsl = DSL.define(file.read, file.path).result
|
58
|
+
end
|
59
|
+
|
60
|
+
return dsl
|
61
|
+
end
|
62
|
+
|
53
63
|
def walk_hosted_zones(dsl)
|
54
64
|
expected = collection_to_hash(dsl.hosted_zones, :name)
|
55
65
|
actual = collection_to_hash(@route53.hosted_zones, :name)
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'roadworker/log'
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
# XXX:
|
7
|
+
unless Socket.const_defined?(:AF_INET6)
|
8
|
+
Socket::AF_INET6 = Socket::AF_INET
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'net/dns'
|
12
|
+
|
13
|
+
module Roadworker
|
14
|
+
class DSL
|
15
|
+
class Tester
|
16
|
+
include Roadworker::Log
|
17
|
+
|
18
|
+
DEFAULT_NAMESERVERS = '8.8.8.8'
|
19
|
+
ASTERISK_PREFIX = 'asterisk-of-wildcard'
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def test(dsl, options)
|
23
|
+
self.new(options).test(dsl)
|
24
|
+
end
|
25
|
+
end # of class method
|
26
|
+
|
27
|
+
def initialize(options)
|
28
|
+
@options = options
|
29
|
+
@resolver = create_resolver
|
30
|
+
end
|
31
|
+
|
32
|
+
def test(dsl)
|
33
|
+
records = fetch_records(dsl)
|
34
|
+
failures = 0
|
35
|
+
error_messages = []
|
36
|
+
|
37
|
+
records.each do |key, rrs|
|
38
|
+
errors = []
|
39
|
+
|
40
|
+
name = asterisk_to_anyname(key[0])
|
41
|
+
type = key[1]
|
42
|
+
|
43
|
+
log(:debug, 'Check DNS', :white, "#{name} #{type}")
|
44
|
+
|
45
|
+
response = query(name, type)
|
46
|
+
|
47
|
+
unless response
|
48
|
+
failures += 1
|
49
|
+
print_failure
|
50
|
+
next
|
51
|
+
end
|
52
|
+
|
53
|
+
is_valid = rrs.any? {|record|
|
54
|
+
expected_value = (record.resource_records || []).map {|i| i[:value].strip }.sort
|
55
|
+
expected_ttl = record.dns_name ? 60 : record.ttl
|
56
|
+
|
57
|
+
actual_value = response.answer.map {|i| (type == 'TXT' ? i.txt : i.value).strip }.sort
|
58
|
+
actual_ttls = response.answer.map {|i| i.ttl }
|
59
|
+
|
60
|
+
case type
|
61
|
+
when 'NS', 'PTR', 'MX', 'CNAME'
|
62
|
+
expected_value = expected_value.map {|i| i.downcase.sub(/\.\Z/, '') }
|
63
|
+
actual_value = actual_value.map {|i| i.downcase.sub(/\.\Z/, '') }
|
64
|
+
when 'TXT'
|
65
|
+
expected_value = expected_value.map {|i| i.scan(/"([^"]+)"/).join.strip.gsub(/\s+/, ' ') }
|
66
|
+
actual_value = actual_value.map {|i| i.strip.gsub(/\s+/, ' ') }
|
67
|
+
end
|
68
|
+
|
69
|
+
expected_message = record.resource_records ? expected_value.map {|i| "#{i}(#{expected_ttl})" }.join(',') : "#{record.dns_name}(#{expected_ttl})"
|
70
|
+
actual_message = actual_value.zip(actual_ttls).map {|v, t| "#{v}(#{t})" }.join(',')
|
71
|
+
logmsg_expected = "expected=#{expected_message}"
|
72
|
+
logmsg_actual = "actual=#{actual_message}"
|
73
|
+
log(:debug, " #{logmsg_expected}\n #{logmsg_actual}", :white, "#{name} #{type}")
|
74
|
+
|
75
|
+
is_same = false
|
76
|
+
|
77
|
+
if record.dns_name
|
78
|
+
# A(Alias)
|
79
|
+
is_same = response.answer.all? {|a|
|
80
|
+
query(a.value, 'PTR').answer.all? do |ptr|
|
81
|
+
ptr.value =~ /\.compute\.amazonaws\.com\.\Z/
|
82
|
+
end
|
83
|
+
}
|
84
|
+
else
|
85
|
+
is_same = (expected_value == actual_value)
|
86
|
+
end
|
87
|
+
|
88
|
+
if is_same
|
89
|
+
unless actual_ttls.all? {|i| i <= expected_ttl }
|
90
|
+
is_same = false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
errors << [logmsg_expected, logmsg_actual] unless is_same
|
95
|
+
is_same
|
96
|
+
}
|
97
|
+
|
98
|
+
if is_valid
|
99
|
+
print_success
|
100
|
+
else
|
101
|
+
failures += 1
|
102
|
+
print_failure
|
103
|
+
|
104
|
+
errors.each do |logmsg_expected, logmsg_actual|
|
105
|
+
error_messages << "#{name} #{type}:\n #{logmsg_expected}\n #{logmsg_actual}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
result &&= is_valid
|
110
|
+
end
|
111
|
+
|
112
|
+
puts unless @options.debug
|
113
|
+
|
114
|
+
error_messages.each do |msg|
|
115
|
+
log(:warn, msg, :intense_red)
|
116
|
+
end
|
117
|
+
|
118
|
+
[records.length, failures]
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def create_resolver
|
124
|
+
log_file = @options.debug ? Net::DNS::Resolver::Defaults[:log_file] : '/dev/null'
|
125
|
+
|
126
|
+
if File.exist?(Net::DNS::Resolver::Defaults[:config_file])
|
127
|
+
Net::DNS::Resolver.new(:log_file => log_file)
|
128
|
+
else
|
129
|
+
Tempfile.open(File.basename(__FILE__)) do |f|
|
130
|
+
Net::DNS::Resolver.new(:config_file => f.path, :nameservers => DEFAULT_NAMESERVERS, :log_file => log_file)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def fetch_records(dsl)
|
136
|
+
record_list = {}
|
137
|
+
|
138
|
+
dsl.hosted_zones.each do |zone|
|
139
|
+
zone.rrsets.each do |record|
|
140
|
+
key = [record.name, record.type]
|
141
|
+
record_list[key] ||= []
|
142
|
+
record_list[key] << record
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
return record_list
|
147
|
+
end
|
148
|
+
|
149
|
+
def asterisk_to_anyname(name)
|
150
|
+
rand_str = (("a".."z").to_a + ("A".."Z").to_a + (0..9).to_a).shuffle[0..7].join
|
151
|
+
name.gsub('*', "#{ASTERISK_PREFIX}-#{rand_str}")
|
152
|
+
end
|
153
|
+
|
154
|
+
def query(name, type)
|
155
|
+
ctype = Net::DNS.const_get(type)
|
156
|
+
response = nil
|
157
|
+
|
158
|
+
begin
|
159
|
+
response = @resolver.query(name, ctype)
|
160
|
+
rescue => e
|
161
|
+
log(:warn, "WARNING #{e.message}", :yellow, "#{name} #{type}")
|
162
|
+
end
|
163
|
+
|
164
|
+
return response
|
165
|
+
end
|
166
|
+
|
167
|
+
def print_success
|
168
|
+
print '.'.intense_green unless @options.debug
|
169
|
+
end
|
170
|
+
|
171
|
+
def print_failure
|
172
|
+
print 'F'.intense_red unless @options.debug
|
173
|
+
end
|
174
|
+
|
175
|
+
end # Tester
|
176
|
+
end # DSL
|
177
|
+
end # Roadworker
|
data/lib/roadworker/dsl.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'roadworker/dsl-converter'
|
2
|
+
require 'roadworker/dsl-tester'
|
2
3
|
|
3
4
|
require 'ostruct'
|
4
5
|
|
@@ -15,6 +16,11 @@ module Roadworker
|
|
15
16
|
def convert(hosted_zones)
|
16
17
|
Converter.convert(hosted_zones)
|
17
18
|
end
|
19
|
+
|
20
|
+
|
21
|
+
def test(dsl, options)
|
22
|
+
Tester.test(dsl, options)
|
23
|
+
end
|
18
24
|
end # of class method
|
19
25
|
|
20
26
|
attr_reader :result
|
data/lib/roadworker/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roadworker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-07-
|
12
|
+
date: 2013-07-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: 1.2.2
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: net-dns
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.8.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.8.0
|
46
62
|
- !ruby/object:Gem::Dependency
|
47
63
|
name: bundler
|
48
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,6 +120,7 @@ files:
|
|
104
120
|
- lib/roadworker/client.rb
|
105
121
|
- lib/roadworker/collection.rb
|
106
122
|
- lib/roadworker/dsl-converter.rb
|
123
|
+
- lib/roadworker/dsl-tester.rb
|
107
124
|
- lib/roadworker/dsl.rb
|
108
125
|
- lib/roadworker/log.rb
|
109
126
|
- lib/roadworker/route53-exporter.rb
|