roadworker 0.1.0 → 0.2.0

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.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