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 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
- export_mode = false
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('-e', '--export') {|v| export_mode = true }
32
- opt.on('-o', '--output FILE') {|v| output_file = v }
33
- opt.on('', '--dry-run') {|v| options[:dry_run] = true }
34
- opt.on('' , '--force') { options[:force] = true }
35
- opt.on('' , '--no-color') { options[:color] = false }
36
- opt.on('' , '--debug') { options[:debug] = true }
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
- if export_mode
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
- else
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
@@ -19,16 +19,7 @@ module Roadworker
19
19
  end
20
20
 
21
21
  def apply(file)
22
- dsl = nil
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Roadworker
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
4
4
 
5
5
  Version = Roadworker::VERSION
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.1.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-29 00:00:00.000000000 Z
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