roadworker 0.0.1
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 +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: []
|