roadworker 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +69 -0
- data/bin/roadwork +89 -0
- data/lib/roadworker.rb +2 -0
- data/lib/roadworker/client.rb +109 -0
- data/lib/roadworker/collection.rb +21 -0
- data/lib/roadworker/dsl-converter.rb +50 -0
- data/lib/roadworker/dsl.rb +119 -0
- data/lib/roadworker/log.rb +12 -0
- data/lib/roadworker/route53-exporter.rb +69 -0
- data/lib/roadworker/route53-ext.rb +32 -0
- data/lib/roadworker/route53-wrapper.rb +253 -0
- data/lib/roadworker/string-ext.rb +28 -0
- data/lib/roadworker/version.rb +5 -0
- metadata +124 -0
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Roadworker
|
2
|
+
|
3
|
+
Roadworker is a tool to manage Route53.
|
4
|
+
|
5
|
+
It defines the state of Route53 using DSL, and updates Route53 according to DSL.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'roadworker'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install roadworker
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```
|
24
|
+
shell> export AWS_ACCESS_KEY_ID='...'
|
25
|
+
shell> export AWS_SECRET_ACCESS_KEY='...'
|
26
|
+
shell> roadwork -e -o Routefile
|
27
|
+
shell> vi Routefile
|
28
|
+
shell> roadwork --dry-run
|
29
|
+
shell> roudwork
|
30
|
+
```
|
31
|
+
|
32
|
+
## Routefile esample
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require 'other/routefile'
|
36
|
+
|
37
|
+
hosted_zone "winebarrel.jp." do
|
38
|
+
rrset "winebarrel.jp.", "MX" do
|
39
|
+
ttl 300
|
40
|
+
resource_records(
|
41
|
+
"10 mx.winebarrel.jp",
|
42
|
+
"10 mx2.winebarrel.jp"
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
rrset "winebarrel.jp.", "NS" do
|
47
|
+
ttl 86400
|
48
|
+
resource_records(
|
49
|
+
"ns-XXX.awsdns-XX.com.",
|
50
|
+
"ns-XXX.awsdns-XX.org.",
|
51
|
+
"ns-XXX.awsdns-XX.net.",
|
52
|
+
"ns-XXX.awsdns-XX.co.uk."
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
rrset "winebarrel.jp.", "SOA" do
|
57
|
+
ttl 86400
|
58
|
+
resource_records(
|
59
|
+
"ns-XXX.awsdns-XX.com. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
hosted_zone "info.winebarrel.jp." do
|
65
|
+
rrset "xxx.info.winebarrel.jp.", "A" do
|
66
|
+
dns_name "elb-dns-name.elb.amazonaws.com"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
data/bin/roadwork
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$: << File.expand_path("#{File.dirname __FILE__}/../lib")
|
3
|
+
require 'rubygems'
|
4
|
+
require 'roadworker'
|
5
|
+
require 'optparse'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
file = 'Routefile'
|
9
|
+
output_file = '-'
|
10
|
+
export_mode = false
|
11
|
+
logger = Logger.new($stdout)
|
12
|
+
|
13
|
+
logger.formatter = proc {|severity, datetime, progname, msg|
|
14
|
+
"#{msg}\n"
|
15
|
+
}
|
16
|
+
options = {
|
17
|
+
:logger => logger,
|
18
|
+
:dry_run => false,
|
19
|
+
:debug => false,
|
20
|
+
:force => false,
|
21
|
+
:color => true,
|
22
|
+
}
|
23
|
+
|
24
|
+
ARGV.options do |opt|
|
25
|
+
access_key = nil
|
26
|
+
secret_key = nil
|
27
|
+
|
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 }
|
37
|
+
opt.parse!
|
38
|
+
|
39
|
+
if access_key and secret_key
|
40
|
+
AWS.config({
|
41
|
+
:access_key_id => access_key,
|
42
|
+
:secret_access_key => secret_key,
|
43
|
+
})
|
44
|
+
elsif (access_key and !secret_key) or (!access_key and secret_key)
|
45
|
+
puts opt.help
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if options[:debug]
|
51
|
+
AWS.config(
|
52
|
+
:http_wire_trace => true,
|
53
|
+
:logger => options[:logger]
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
client = Roadworker::Client.new(options)
|
59
|
+
|
60
|
+
if export_mode
|
61
|
+
exported = client.export
|
62
|
+
|
63
|
+
if output_file == '-'
|
64
|
+
logger.info('Export Route53')
|
65
|
+
puts client.export
|
66
|
+
else
|
67
|
+
logger.info("Export Route53 to `#{output_file}`")
|
68
|
+
open(output_file, 'wb') {|f| f.puts client.export }
|
69
|
+
end
|
70
|
+
else
|
71
|
+
unless File.exist?(file)
|
72
|
+
raise "No Routefile found (looking for: #{file})"
|
73
|
+
end
|
74
|
+
|
75
|
+
msg = "Apply `#{file}` to Route53"
|
76
|
+
msg << ' (dry-run)' if options[:dry_run]
|
77
|
+
logger.info(msg)
|
78
|
+
|
79
|
+
updated = client.apply(file)
|
80
|
+
|
81
|
+
logger.info('There is no update') unless updated
|
82
|
+
end
|
83
|
+
rescue => e
|
84
|
+
if options[:debug]
|
85
|
+
raise e
|
86
|
+
else
|
87
|
+
$stderr.puts e
|
88
|
+
end
|
89
|
+
end
|
data/lib/roadworker.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'roadworker/string-ext'
|
2
|
+
require 'roadworker/dsl'
|
3
|
+
require 'roadworker/log'
|
4
|
+
require 'roadworker/route53-wrapper'
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
require 'ostruct'
|
8
|
+
|
9
|
+
module Roadworker
|
10
|
+
class Client
|
11
|
+
include Roadworker::Log
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@options = OpenStruct.new(options)
|
15
|
+
@options.logger ||= Logger.new($stdout)
|
16
|
+
String.colorize = @options.color
|
17
|
+
@options.route53 = AWS::Route53.new
|
18
|
+
@route53 = Route53Wrapper.new(@options)
|
19
|
+
end
|
20
|
+
|
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
|
+
|
32
|
+
updated = false
|
33
|
+
|
34
|
+
if dsl.hosted_zones.empty? and not @options.force
|
35
|
+
log(:warn, "Nothing is defined (pass `--force` if you want to remove)", :yellow)
|
36
|
+
else
|
37
|
+
AWS.memoize {
|
38
|
+
walk_hosted_zones(dsl)
|
39
|
+
updated = @options.updated
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
return updated
|
44
|
+
end
|
45
|
+
|
46
|
+
def export
|
47
|
+
exported = AWS.memoize { @route53.export }
|
48
|
+
DSL.convert(exported)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def walk_hosted_zones(dsl)
|
54
|
+
expected = collection_to_hash(dsl.hosted_zones, :name)
|
55
|
+
actual = collection_to_hash(@route53.hosted_zones, :name)
|
56
|
+
|
57
|
+
expected.each do |keys, expected_zone|
|
58
|
+
name = keys[0]
|
59
|
+
actual_zone = actual.delete(keys) || @route53.hosted_zones.create(name)
|
60
|
+
walk_rrsets(expected_zone, actual_zone)
|
61
|
+
end
|
62
|
+
|
63
|
+
actual.each do |keys, zone|
|
64
|
+
zone.rrsets.each do |record|
|
65
|
+
record.delete(:cascaded => true)
|
66
|
+
end
|
67
|
+
|
68
|
+
zone.delete
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def walk_rrsets(expected_zone, actual_zone)
|
73
|
+
expected = collection_to_hash(expected_zone.rrsets, :name, :type, :set_identifier)
|
74
|
+
actual = collection_to_hash(actual_zone.rrsets, :name, :type, :set_identifier)
|
75
|
+
|
76
|
+
expected.each do |keys, expected_record|
|
77
|
+
name = keys[0]
|
78
|
+
type = keys[1]
|
79
|
+
set_identifier = keys[2]
|
80
|
+
|
81
|
+
actual_record = actual.delete(keys)
|
82
|
+
|
83
|
+
if actual_record
|
84
|
+
unless actual_record.eql?(expected_record)
|
85
|
+
actual_record.update(expected_record)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
actual_record = actual_zone.rrsets.create(name, type, expected_record)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
actual.each do |keys, record|
|
93
|
+
record.delete
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def collection_to_hash(collection, *keys)
|
98
|
+
hash = {}
|
99
|
+
|
100
|
+
collection.each do |item|
|
101
|
+
key_list = keys.map {|k| item.send(k) }
|
102
|
+
hash[key_list] = item
|
103
|
+
end
|
104
|
+
|
105
|
+
return hash
|
106
|
+
end
|
107
|
+
|
108
|
+
end # Client
|
109
|
+
end # Roadworker
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Roadworker
|
2
|
+
class Collection
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def batch(collection)
|
6
|
+
if collection.respond_to?(:each_batch)
|
7
|
+
collection.each_batch do |batch|
|
8
|
+
batch.each do |item|
|
9
|
+
yield(item)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
else
|
13
|
+
collection.each do |item|
|
14
|
+
yield(item)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end # of class method
|
19
|
+
|
20
|
+
end # Collection
|
21
|
+
end # Roadworker
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Roadworker
|
2
|
+
class DSL
|
3
|
+
class Converter
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def convert(hosted_zones)
|
7
|
+
hosted_zones.map {|i| output_zone(i) }.join("\n")
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def output_rrset(recrod)
|
13
|
+
name = recrod.delete(:name).inspect
|
14
|
+
type = recrod.delete(:type).inspect
|
15
|
+
|
16
|
+
attrs = recrod.map {|key, value|
|
17
|
+
if value.kind_of?(Array)
|
18
|
+
if value.empty?
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
value = value.map {|i| i.inspect }.join(",\n ")
|
22
|
+
"#{key}(\n #{value}\n )"
|
23
|
+
end
|
24
|
+
else
|
25
|
+
"#{key} #{value.inspect}"
|
26
|
+
end
|
27
|
+
}.select {|i| i }.join("\n ")
|
28
|
+
|
29
|
+
return(<<-EOS)
|
30
|
+
rrset #{name}, #{type} do
|
31
|
+
#{attrs}
|
32
|
+
end
|
33
|
+
EOS
|
34
|
+
end
|
35
|
+
|
36
|
+
def output_zone(zone)
|
37
|
+
name = zone[:name].inspect
|
38
|
+
rrsets = zone[:rrsets]
|
39
|
+
|
40
|
+
return(<<-EOS)
|
41
|
+
hosted_zone #{name} do
|
42
|
+
#{rrsets.map {|i| output_rrset(i) }.join("\n").chomp}
|
43
|
+
end
|
44
|
+
EOS
|
45
|
+
end
|
46
|
+
end # of class method
|
47
|
+
|
48
|
+
end # Converter
|
49
|
+
end # DSL
|
50
|
+
end # Roadworker
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'roadworker/dsl-converter'
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module Roadworker
|
6
|
+
class DSL
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def define(source, path)
|
10
|
+
self.new(path) do
|
11
|
+
eval(source, binding)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def convert(hosted_zones)
|
16
|
+
Converter.convert(hosted_zones)
|
17
|
+
end
|
18
|
+
end # of class method
|
19
|
+
|
20
|
+
attr_reader :result
|
21
|
+
|
22
|
+
def initialize(path, &block)
|
23
|
+
@path = path
|
24
|
+
@result = OpenStruct.new({:hosted_zones => []})
|
25
|
+
instance_eval(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def require(file)
|
31
|
+
routefile = File.expand_path(File.join(File.dirname(@path), file))
|
32
|
+
|
33
|
+
if File.exist?(routefile)
|
34
|
+
instance_eval(File.read(routefile))
|
35
|
+
else
|
36
|
+
Kernel.require(file)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def hosted_zone(name, &block)
|
41
|
+
@result.hosted_zones << HostedZone.new(name, &block).result
|
42
|
+
end
|
43
|
+
|
44
|
+
class HostedZone
|
45
|
+
attr_reader :result
|
46
|
+
|
47
|
+
def initialize(name, &block)
|
48
|
+
@name = name
|
49
|
+
rrsets = []
|
50
|
+
|
51
|
+
@result = OpenStruct.new({
|
52
|
+
:name => name,
|
53
|
+
:resource_record_sets => rrsets,
|
54
|
+
:rrsets => rrsets,
|
55
|
+
})
|
56
|
+
|
57
|
+
instance_eval(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def resource_record_set(rrset_name, type, &block)
|
63
|
+
if rrset_name.gsub(/\.\Z/, '') !~ /#{Regexp.escape(@name.gsub(/\.\Z/, ''))}\Z/i
|
64
|
+
raise "Invalid ResourceRecordSet Name: #{rrset_name}"
|
65
|
+
end
|
66
|
+
|
67
|
+
@result.resource_record_sets << ResourceRecordSet.new(rrset_name, type, &block).result
|
68
|
+
end
|
69
|
+
alias rrset resource_record_set
|
70
|
+
|
71
|
+
class ResourceRecordSet
|
72
|
+
attr_reader :result
|
73
|
+
|
74
|
+
def initialize(name, type, &block)
|
75
|
+
@result = OpenStruct.new({
|
76
|
+
:name => name,
|
77
|
+
:type => type,
|
78
|
+
})
|
79
|
+
|
80
|
+
instance_eval(&block)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def set_identifier(value = nil)
|
86
|
+
@result.set_identifier = value
|
87
|
+
end
|
88
|
+
alias identifier set_identifier
|
89
|
+
|
90
|
+
def weight(value)
|
91
|
+
@result.weight = value
|
92
|
+
end
|
93
|
+
|
94
|
+
def ttl(value)
|
95
|
+
@result.ttl = value
|
96
|
+
end
|
97
|
+
|
98
|
+
def region(value)
|
99
|
+
@result.region = value
|
100
|
+
end
|
101
|
+
|
102
|
+
def dns_name(value)
|
103
|
+
@result.dns_name = value
|
104
|
+
end
|
105
|
+
|
106
|
+
def resource_records(*values)
|
107
|
+
if values.uniq.length != values.length
|
108
|
+
raise "Duplicate ResourceRecords: #{values.join(', ')}"
|
109
|
+
end
|
110
|
+
|
111
|
+
@result.resource_records = [values].flatten.map {|i| {:value => i} }
|
112
|
+
end
|
113
|
+
|
114
|
+
end # ResourceRecordSet
|
115
|
+
|
116
|
+
end # HostedZone
|
117
|
+
|
118
|
+
end # DSL
|
119
|
+
end # RoadWorker
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Roadworker
|
2
|
+
module Log
|
3
|
+
|
4
|
+
def log(level, message, color, log_id = nil)
|
5
|
+
log_id = yield if block_given?
|
6
|
+
message = "#{message}: #{log_id}"
|
7
|
+
message << ' (dry-run)' if @options.dry_run
|
8
|
+
@options.logger.send(level, message.send(color))
|
9
|
+
end
|
10
|
+
|
11
|
+
end # Log
|
12
|
+
end # Roadworker
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'roadworker/collection'
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module Roadworker
|
6
|
+
class Exporter
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def export(route53)
|
10
|
+
self.new(route53).export
|
11
|
+
end
|
12
|
+
end # of class method
|
13
|
+
|
14
|
+
def initialize(route53)
|
15
|
+
@route53 = route53
|
16
|
+
end
|
17
|
+
|
18
|
+
def export
|
19
|
+
result = []
|
20
|
+
|
21
|
+
Collection.batch(@route53.hosted_zones) do |zone|
|
22
|
+
zone_h = item_to_hash(zone, :name)
|
23
|
+
result << zone_h
|
24
|
+
|
25
|
+
rrsets = []
|
26
|
+
zone_h[:rrsets] = rrsets
|
27
|
+
|
28
|
+
Collection.batch(zone.rrsets) do |record|
|
29
|
+
attrs = [
|
30
|
+
:name,
|
31
|
+
:type,
|
32
|
+
:set_identifier,
|
33
|
+
:weight,
|
34
|
+
:ttl,
|
35
|
+
:resource_records,
|
36
|
+
:alias_target,
|
37
|
+
:region,
|
38
|
+
]
|
39
|
+
|
40
|
+
record_h = item_to_hash(record, *attrs)
|
41
|
+
rrsets << record_h
|
42
|
+
|
43
|
+
rrs = record_h.delete(:resource_records)
|
44
|
+
record_h[:resource_records] = rrs.map {|i| i[:value] }
|
45
|
+
|
46
|
+
if (alias_target = record_h.delete(:alias_target))
|
47
|
+
record_h[:dns_name] = alias_target[:dns_name]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
return result
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def item_to_hash(item, *attrs)
|
58
|
+
h = {}
|
59
|
+
|
60
|
+
attrs.each do |attr|
|
61
|
+
value = item.send(attr)
|
62
|
+
h[attr] = value if value
|
63
|
+
end
|
64
|
+
|
65
|
+
return h
|
66
|
+
end
|
67
|
+
|
68
|
+
end # Exporter
|
69
|
+
end # Roadworker
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module AWS
|
4
|
+
class Route53
|
5
|
+
|
6
|
+
def dns_name_to_alias_target(name)
|
7
|
+
name = name.gsub(/\.\Z/, '')
|
8
|
+
|
9
|
+
unless name =~ /([^.]+)\.elb\.amazonaws.com\Z/i
|
10
|
+
raise "Invalid DNS Name: #{name}"
|
11
|
+
end
|
12
|
+
|
13
|
+
region = $1.downcase
|
14
|
+
elb = AWS::ELB.new(:region => region)
|
15
|
+
|
16
|
+
load_balancer = elb.load_balancers.find do |lb|
|
17
|
+
lb.dns_name == name
|
18
|
+
end
|
19
|
+
|
20
|
+
unless load_balancer
|
21
|
+
raise "Cannot find ELB: #{name}"
|
22
|
+
end
|
23
|
+
|
24
|
+
{
|
25
|
+
:hosted_zone_id => load_balancer.canonical_hosted_zone_name_id,
|
26
|
+
:dns_name => load_balancer.dns_name,
|
27
|
+
:evaluate_target_health => false, # XXX:
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
end # Route53
|
32
|
+
end # Roadworker
|
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'roadworker/collection'
|
2
|
+
require 'roadworker/log'
|
3
|
+
require 'roadworker/route53-exporter'
|
4
|
+
require 'roadworker/route53-ext'
|
5
|
+
|
6
|
+
require 'ostruct'
|
7
|
+
|
8
|
+
module Roadworker
|
9
|
+
class Route53Wrapper
|
10
|
+
|
11
|
+
RRSET_ATTRS = [
|
12
|
+
:set_identifier,
|
13
|
+
:weight,
|
14
|
+
:ttl,
|
15
|
+
:resource_records,
|
16
|
+
:dns_name,
|
17
|
+
:region
|
18
|
+
]
|
19
|
+
|
20
|
+
def initialize(options)
|
21
|
+
@options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def export
|
25
|
+
Exporter.export(@options.route53)
|
26
|
+
end
|
27
|
+
|
28
|
+
def hosted_zones
|
29
|
+
HostedZoneCollectionWrapper.new(@options.route53.hosted_zones, @options)
|
30
|
+
end
|
31
|
+
|
32
|
+
class HostedZoneCollectionWrapper
|
33
|
+
include Roadworker::Log
|
34
|
+
|
35
|
+
def initialize(hosted_zones, options)
|
36
|
+
@hosted_zones = hosted_zones
|
37
|
+
@options = options
|
38
|
+
end
|
39
|
+
|
40
|
+
def each
|
41
|
+
Collection.batch(@hosted_zones) do |zone|
|
42
|
+
yield(HostedZoneWrapper.new(zone, @options))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def create(name, opts = {})
|
47
|
+
log(:info, 'Create HostedZone', :cyan, name)
|
48
|
+
|
49
|
+
if @options.dry_run
|
50
|
+
zone = OpenStruct.new({:name => name, :rrsets => []}.merge(opts))
|
51
|
+
else
|
52
|
+
zone = @hosted_zones.create(name, opts)
|
53
|
+
@options.updated = true
|
54
|
+
end
|
55
|
+
|
56
|
+
HostedZoneWrapper.new(zone, @options)
|
57
|
+
end
|
58
|
+
end # HostedZoneCollection
|
59
|
+
|
60
|
+
class HostedZoneWrapper
|
61
|
+
include Roadworker::Log
|
62
|
+
|
63
|
+
def initialize(hosted_zone, options)
|
64
|
+
@hosted_zone = hosted_zone
|
65
|
+
@options = options
|
66
|
+
end
|
67
|
+
|
68
|
+
def resource_record_sets
|
69
|
+
ResourceRecordSetCollectionWrapper.new(@hosted_zone.rrsets, @options)
|
70
|
+
end
|
71
|
+
alias rrsets resource_record_sets
|
72
|
+
|
73
|
+
def delete
|
74
|
+
if @options.force
|
75
|
+
log(:info, 'Delete HostedZone', :red, @hosted_zone.name)
|
76
|
+
|
77
|
+
unless @options.dry_run
|
78
|
+
@hosted_zone.delete
|
79
|
+
@options.updated = true
|
80
|
+
end
|
81
|
+
else
|
82
|
+
log(:info, 'Undefined HostedZone (pass `--force` if you want to remove)', :yellow, @hosted_zone.name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def method_missing(method_name, *args)
|
89
|
+
@hosted_zone.send(method_name, *args)
|
90
|
+
end
|
91
|
+
end # HostedZoneWrapper
|
92
|
+
|
93
|
+
class ResourceRecordSetCollectionWrapper
|
94
|
+
include Roadworker::Log
|
95
|
+
|
96
|
+
def initialize(resource_record_sets, options)
|
97
|
+
@resource_record_sets = resource_record_sets
|
98
|
+
@options = options
|
99
|
+
end
|
100
|
+
|
101
|
+
def each
|
102
|
+
Collection.batch(@resource_record_sets) do |record|
|
103
|
+
yield(ResourceRecordSetWrapper.new(record, @options))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def create(name, type, expected_record)
|
108
|
+
log(:info, 'Create ResourceRecordSet', :cyan) do
|
109
|
+
log_id = [name, type].join(' ')
|
110
|
+
rrset_setid = expected_record.set_identifier
|
111
|
+
rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
|
112
|
+
end
|
113
|
+
|
114
|
+
if @options.dry_run
|
115
|
+
record = expected_record
|
116
|
+
else
|
117
|
+
opts = {}
|
118
|
+
|
119
|
+
Route53Wrapper::RRSET_ATTRS.each do |attr|
|
120
|
+
value = expected_record.send(attr)
|
121
|
+
next unless value
|
122
|
+
|
123
|
+
case attr
|
124
|
+
when :dns_name
|
125
|
+
attr = :alias_target
|
126
|
+
value = @options.route53.dns_name_to_alias_target(value)
|
127
|
+
end
|
128
|
+
|
129
|
+
opts[attr] = value
|
130
|
+
end
|
131
|
+
|
132
|
+
record = @resource_record_sets.create(name, type, opts)
|
133
|
+
@options.updated = true
|
134
|
+
end
|
135
|
+
|
136
|
+
ResourceRecordSetWrapper.new(record, @options)
|
137
|
+
end
|
138
|
+
end # ResourceRecordSetCollectionWrapper
|
139
|
+
|
140
|
+
class ResourceRecordSetWrapper
|
141
|
+
include Roadworker::Log
|
142
|
+
|
143
|
+
def initialize(resource_record_set, options)
|
144
|
+
@resource_record_set = resource_record_set
|
145
|
+
@options = options
|
146
|
+
end
|
147
|
+
|
148
|
+
def eql?(expected_record)
|
149
|
+
Route53Wrapper::RRSET_ATTRS.all? do |attr|
|
150
|
+
expected = expected_record.send(attr)
|
151
|
+
expected = nil if expected.kind_of?(Array) && expected.empty?
|
152
|
+
actual = self.send(attr)
|
153
|
+
actual = nil if actual.kind_of?(Array) && actual.empty?
|
154
|
+
|
155
|
+
if !expected and !actual
|
156
|
+
true
|
157
|
+
elsif expected and actual
|
158
|
+
case attr
|
159
|
+
when :resource_records
|
160
|
+
expected = expected.sort_by {|i| i[:value] }
|
161
|
+
actual = actual.sort_by {|i| i[:value] }
|
162
|
+
end
|
163
|
+
|
164
|
+
(expected == actual)
|
165
|
+
else
|
166
|
+
false
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def update(expected_record)
|
172
|
+
log(:info, 'Update ResourceRecordSet', :green) do
|
173
|
+
log_id = [@resource_record_set.name, @resource_record_set.type].join(' ')
|
174
|
+
rrset_setid = @resource_record_set.set_identifier
|
175
|
+
rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
|
176
|
+
end
|
177
|
+
|
178
|
+
Route53Wrapper::RRSET_ATTRS.each do |attr|
|
179
|
+
expected = expected_record.send(attr)
|
180
|
+
expected = nil if expected.kind_of?(Array) && expected.empty?
|
181
|
+
actual = self.send(attr)
|
182
|
+
actual = nil if actual.kind_of?(Array) && actual.empty?
|
183
|
+
|
184
|
+
if (expected and !actual) or (!expected and actual)
|
185
|
+
log(:info, " set #{attr}=#{expected.inspect}" , :green) do
|
186
|
+
log_id = [@resource_record_set.name, @resource_record_set.type].join(' ')
|
187
|
+
rrset_setid = @resource_record_set.set_identifier
|
188
|
+
rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
|
189
|
+
end
|
190
|
+
|
191
|
+
self.send(:"#{attr}=", expected) unless @options.dry_run
|
192
|
+
elsif expected and actual
|
193
|
+
case attr
|
194
|
+
when :resource_records
|
195
|
+
expected = expected.sort_by {|i| i[:value] }
|
196
|
+
actual = actual.sort_by {|i| i[:value] }
|
197
|
+
end
|
198
|
+
|
199
|
+
if expected != actual
|
200
|
+
log(:info, " set #{attr}=#{expected.inspect}" , :green) do
|
201
|
+
log_id = [@resource_record_set.name, @resource_record_set.type].join(' ')
|
202
|
+
rrset_setid = @resource_record_set.set_identifier
|
203
|
+
rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
|
204
|
+
end
|
205
|
+
|
206
|
+
self.send(:"#{attr}=", expected) unless @options.dry_run
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
unless @options.dry_run
|
212
|
+
@resource_record_set.update
|
213
|
+
@options.updated = true
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def delete(opts = {})
|
218
|
+
return if type =~ /\A(SOA|NS)\Z/i
|
219
|
+
if not opts[:cascaded] or @options.force
|
220
|
+
log(:info, 'Delete ResourceRecordSet', :red) do
|
221
|
+
log_id = [@resource_record_set.name, @resource_record_set.type].join(' ')
|
222
|
+
rrset_setid = @resource_record_set.set_identifier
|
223
|
+
rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
unless @options.dry_run
|
228
|
+
@resource_record_set.delete
|
229
|
+
@options.updated = true
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def dns_name=(name)
|
234
|
+
if name
|
235
|
+
@resource_record_set.alias_target = @options.route53.dns_name_to_alias_target(name)
|
236
|
+
else
|
237
|
+
@resource_record_set.alias_target = nil
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def dns_name
|
242
|
+
(@resource_record_set.alias_target || {})[:dns_name]
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
def method_missing(method_name, *args)
|
248
|
+
@resource_record_set.send(method_name, *args)
|
249
|
+
end
|
250
|
+
end # ResourceRecordSetWrapper
|
251
|
+
|
252
|
+
end # Route53Wrapper
|
253
|
+
end # Roadworker
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'term/ansicolor'
|
2
|
+
|
3
|
+
class String
|
4
|
+
@@colorize = false
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def colorize=(value)
|
8
|
+
@@colorize = value
|
9
|
+
end
|
10
|
+
|
11
|
+
def colorize
|
12
|
+
@@colorize
|
13
|
+
end
|
14
|
+
end # of class method
|
15
|
+
|
16
|
+
Term::ANSIColor::Attribute.named_attributes.map do |attr|
|
17
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
18
|
+
def #{attr.name}
|
19
|
+
if @@colorize
|
20
|
+
Term::ANSIColor.send(#{attr.name.inspect}, self)
|
21
|
+
else
|
22
|
+
self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
EOS
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: roadworker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- winebarrel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: aws-sdk
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: term-ansicolor
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: bundler
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.3'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Roadworker is a tool to manage Route53. It defines the state of Route53
|
79
|
+
using DSL, and updates Route53 according to DSL.
|
80
|
+
email: sgwr_dts@yahoo.co.jp
|
81
|
+
executables:
|
82
|
+
- roadwork
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- README.md
|
87
|
+
- bin/roadwork
|
88
|
+
- lib/roadworker/client.rb
|
89
|
+
- lib/roadworker/collection.rb
|
90
|
+
- lib/roadworker/dsl-converter.rb
|
91
|
+
- lib/roadworker/dsl.rb
|
92
|
+
- lib/roadworker/log.rb
|
93
|
+
- lib/roadworker/route53-exporter.rb
|
94
|
+
- lib/roadworker/route53-ext.rb
|
95
|
+
- lib/roadworker/route53-wrapper.rb
|
96
|
+
- lib/roadworker/string-ext.rb
|
97
|
+
- lib/roadworker/version.rb
|
98
|
+
- lib/roadworker.rb
|
99
|
+
homepage: https://bitbucket.org/winebarrel/roadworker
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 1.8.23
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Roadworker is a tool to manage Route53.
|
124
|
+
test_files: []
|