mappru 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4f5864c5b6cc52cc629a4ae35a83f18f399ef296
4
+ data.tar.gz: 03d30ac958f959f74900786ac356cb2f1a3e97ab
5
+ SHA512:
6
+ metadata.gz: 5ded9bd4f3118d025cb55b9dadf41b92c7fb045ca9004ef454a39444333e6f429e2dadc97460fb5be898e544291aa87a2767c0e46386012f26734f65f4286682
7
+ data.tar.gz: 38a59ce1b8050cfbeda278ce4092eaddefabb414fed9935a2a9cc04648947aecfdf7dad3ca462977a47a6c6e73ec7fce5304a58d5bed0248afd0cef99a0656f4
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ test.rb
11
+ RouteTable
12
+ *.table
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.4
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mappru.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 winebarrel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Mappru
2
+
3
+ Mappru is a tool to manage VPC Route Table.
4
+
5
+ It defines the state of VPC Route Table using DSL, and updates VPC Route Table according to DSL.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'mappru'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install mappru
22
+
23
+ ## Usage
24
+
25
+ ```sh
26
+ export AWS_ACCESS_KEY_ID='...'
27
+ export AWS_SECRET_ACCESS_KEY='...'
28
+ mappru -e -o RouteTable # export VPC Route Table
29
+ vi PolicyFile
30
+ mappru -a --dry-run
31
+ mappru -a # apply `RouteTable`
32
+ ```
33
+
34
+ ## Help
35
+
36
+ ```
37
+ Usage: mappru [options]
38
+ -k, --access-key ACCESS_KEY
39
+ -s, --secret-key SECRET_KEY
40
+ -r, --region REGION
41
+ --profile PROFILE
42
+ --credentials-path PATH
43
+ -a, --apply
44
+ -f, --file FILE
45
+ --dry-run
46
+ -e, --export
47
+ -o, --output FILE
48
+ --split
49
+ --vpc-id REGEXP
50
+ --rt-name REGEXP
51
+ --no-color
52
+ --debug
53
+ ```
54
+
55
+ ## RouteTable example
56
+
57
+ ```ruby
58
+ require 'other/tablefile'
59
+
60
+ vpc "vpc-12345678" do
61
+ route_table "foo-rt" do
62
+ subnets "subnet-12345678"
63
+ route destination_cidr_block: "0.0.0.0/0", gateway_id: "igw-12345678"
64
+ route destination_cidr_block: "192.168.100.101/32", network_interface_id: "eni-12345678"
65
+ end
66
+
67
+ route_table "bar-rt" do
68
+ subnets "subnet-87654321"
69
+ route destination_cidr_block: "192.168.100.102/32", network_interface_id: "eni-87654321"
70
+ end
71
+
72
+ # Undefined Route Table will be ignored
73
+ end
74
+
75
+ # Undefined VPC will be ignored
76
+ ```
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mappru"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/mappru ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'mappru'
5
+ require 'optparse'
6
+
7
+ Version = Mappru::VERSION
8
+
9
+ DEFAULT_FILENAME = 'RouteTable'
10
+
11
+ MAGIC_COMMENT = <<-EOS
12
+ # -*- mode: ruby -*-
13
+ # vi: set ft=ruby :
14
+ EOS
15
+
16
+ def parse_options(argv)
17
+ options = {
18
+ file: DEFAULT_FILENAME,
19
+ output: '-',
20
+ color: true,
21
+ aws: {},
22
+ }
23
+
24
+ opt = OptionParser.new
25
+ opt.on('-k', '--access-key ACCESS_KEY') {|v| options[:aws][:access_key_id] = v }
26
+ opt.on('-s', '--secret-key SECRET_KEY') {|v| options[:aws][:secret_access_key] = v }
27
+ opt.on('-r', '--region REGION') {|v| options[:aws][:region] = v }
28
+
29
+ opt.on('', '--profile PROFILE') do |v|
30
+ options[:aws][:credentials] ||= {}
31
+ options[:aws][:credentials][:profile_name] = v
32
+ end
33
+
34
+ opt.on('', '--credentials-path PATH') do |v|
35
+ options[:aws][:credentials] ||= {}
36
+ options[:aws][:credentials][:path] = v
37
+ end
38
+
39
+ opt.on('-a', '--apply') { options[:mode] = :apply }
40
+ opt.on('-f', '--file FILE') {|v| options[:file] = v }
41
+ opt.on('' , '--dry-run') { options[:dry_run] = true }
42
+ opt.on('-e', '--export') { options[:mode] = :export }
43
+ opt.on('-o', '--output FILE') {|v| options[:output] = v }
44
+ opt.on('' , '--split') { options[:split] = :true }
45
+ opt.on('' , '--vpc-id REGEXP') {|v| options[:vpc_id] = Regexp.new(v) }
46
+ opt.on('' , '--rt-name REGEXP') {|v| options[:rt_name] = Regexp.new(v) }
47
+ opt.on('' , '--no-color') { options[:color] = false }
48
+ opt.on('' , '--debug') { options[:debug] = true }
49
+
50
+ opt.parse!(argv)
51
+
52
+ unless options[:mode]
53
+ puts opt.help
54
+ exit 1
55
+ end
56
+
57
+ if options[:aws][:credentials]
58
+ credentials = Aws::SharedCredentials.new(options[:aws][:credentials])
59
+ options[:aws][:credentials] = credentials
60
+ end
61
+
62
+ Aws.config.update(options[:aws])
63
+ String.colorize = options[:color]
64
+
65
+ if options[:debug]
66
+ Mappru::Logger.instance.set_debug(options[:debug])
67
+
68
+ Aws.config.update(
69
+ :http_wire_trace => true,
70
+ :logger => Mappru::Logger.instance
71
+ )
72
+ end
73
+
74
+ options
75
+ rescue => e
76
+ $stderr.puts("[ERROR] #{e.message}")
77
+ exit 1
78
+ end
79
+
80
+ def main(argv)
81
+ options = parse_options(argv)
82
+ client = Mappru::Client.new(options)
83
+ logger = Mappru::Logger.instance
84
+
85
+ case options[:mode]
86
+ when :export
87
+ exported = client.export
88
+ output = options[:output]
89
+
90
+ if options[:split]
91
+ logger.info('Export Route Table')
92
+
93
+ output = DEFAULT_FILENAME if output == '-'
94
+ dir = File.dirname(output)
95
+ FileUtils.mkdir_p(dir)
96
+ requires = []
97
+
98
+ exported.each do |vpc_id, rts|
99
+ vpc_dir = File.join(dir, vpc_id)
100
+ FileUtils.mkdir_p(vpc_id)
101
+
102
+ rts.each do |rt_name, attrs|
103
+ filename = "#{rt_name}.table"
104
+ requires << File.join(vpc_id, filename)
105
+ rt_file = File.join(vpc_dir, filename)
106
+
107
+ logger.info(" write `#{rt_file}`")
108
+
109
+ dsl = Mappru::DSL.convert({vpc_id => {rt_name => attrs}}, options)
110
+
111
+ open(rt_file, 'wb') do |f|
112
+ f.puts MAGIC_COMMENT
113
+ f.puts dsl
114
+ end
115
+ end
116
+ end
117
+
118
+ logger.info(" write `#{output}`")
119
+
120
+ open(output, 'wb') do |f|
121
+ f.puts MAGIC_COMMENT
122
+
123
+ requires.each do |rt_file|
124
+ f.puts "require '#{rt_file}'"
125
+ end
126
+ end
127
+ else
128
+ dsl = Mappru::DSL.convert(exported, options)
129
+
130
+ if output == '-'
131
+ logger.info('# Export Route Table')
132
+ puts dsl
133
+ else
134
+ logger.info("Export Route Table to `#{output}`")
135
+ open(output, 'wb') do |f|
136
+ f.puts MAGIC_COMMENT
137
+ f.puts dsl
138
+ end
139
+ end
140
+ end
141
+ when :apply
142
+ file = options[:file]
143
+
144
+ unless File.exist?(file)
145
+ raise "No RouteTable found (looking for: #{file})"
146
+ end
147
+
148
+ message = "Apply `#{file}` to Route Table"
149
+ message << ' (dry-run)' if options[:dry_run]
150
+ logger.info(message)
151
+
152
+ updated = client.apply(file)
153
+
154
+ logger.info('No change'.intense_blue) unless updated
155
+ else
156
+ raise "Unknown mode: #{options[:mode]}"
157
+ end
158
+ rescue => e
159
+ if options[:debug]
160
+ raise e
161
+ else
162
+ $stderr.puts("[ERROR] #{e.message}".red)
163
+ exit 1
164
+ end
165
+ end
166
+
167
+ main(ARGV)
@@ -0,0 +1,163 @@
1
+ class Mappru::Client
2
+ include Mappru::Logger::Helper
3
+ include Mappru::Utils::Helper
4
+
5
+ def initialize(options = {})
6
+ @options = options
7
+ @client = @options[:client] || Aws::EC2::Client.new
8
+ @resource = Aws::EC2::Resource.new(client: @client)
9
+ @driver = Mappru::Driver.new(@client, @options)
10
+ end
11
+
12
+ def export
13
+ Mappru::Exporter.export(@client, @options)
14
+ end
15
+
16
+ def apply(file)
17
+ walk(file)
18
+ end
19
+
20
+ private
21
+
22
+ def walk(file)
23
+ expected = load_file(file)
24
+ actual = Mappru::Exporter.export(@client, @options)
25
+
26
+ updated = walk_vpcs(expected, actual)
27
+
28
+ if @options[:dry_run]
29
+ false
30
+ else
31
+ updated
32
+ end
33
+ end
34
+
35
+ def walk_vpcs(expected, actual)
36
+ updated = false
37
+
38
+ expected.each do |vpc_id, expected_rts|
39
+ next unless matched?(vpc_id, @options[:vpc_id])
40
+
41
+ actual_rts = actual.delete(vpc_id)
42
+
43
+ if actual_rts
44
+ updated = walk_vpc(vpc_id, expected_rts, actual_rts) || updated
45
+ else
46
+ log(:warn, "No such VPC: #{vpc_id}")
47
+ end
48
+ end
49
+
50
+ # Undefined VPC will be ignored
51
+
52
+ updated
53
+ end
54
+
55
+ def walk_vpc(vpc_id, expected, actual)
56
+ updated = false
57
+
58
+ expected.each do |rt_name, expected_rt|
59
+ next unless matched?(rt_name, @options[:rt_name])
60
+
61
+ actual_rt = actual.delete(rt_name)
62
+
63
+ unless actual_rt
64
+ actual_rt = @driver.create_route_table(vpc_id, rt_name, expected_rt)
65
+ updated = true
66
+ end
67
+
68
+ updated = walk_route_table(vpc_id, rt_name, expected_rt, actual_rt) || updated
69
+ end
70
+
71
+ # Undefined Route Table will be ignored
72
+
73
+ updated
74
+ end
75
+
76
+ def walk_route_table(vpc_id, name, expected, actual)
77
+ updated = false
78
+ updated = walk_subnets(vpc_id, name, expected[:subnets], actual[:subnets]) || updated
79
+ updated = walk_routes(vpc_id, name, expected[:routes], actual[:routes]) || updated
80
+ updated
81
+ end
82
+
83
+ def walk_subnets(vpc_id, name, expected, actual)
84
+ updated = false
85
+ assoc_subnets = expected - actual
86
+ disassoc_subnets = actual - expected
87
+
88
+ unless assoc_subnets.empty?
89
+ @driver.associate_subnets(vpc_id, name, assoc_subnets)
90
+ updated = true
91
+ end
92
+
93
+ unless disassoc_subnets.empty?
94
+ @driver.disassociate_subnets(vpc_id, name, disassoc_subnets)
95
+ updated = true
96
+ end
97
+
98
+ updated
99
+ end
100
+
101
+ def walk_routes(vpc_id, rt_name, expected, actual)
102
+ expected = routes_to_hash(expected)
103
+ actual = routes_to_hash(actual)
104
+ updated = false
105
+
106
+ expected.each do |dest_cidr, expected_route|
107
+ if expected_route[:gateway_id] == 'local'
108
+ log(:warn, %!Cannot configure "local" target: #{vpc_id}: #{rt_name}!, color: :yellow)
109
+ next
110
+ end
111
+
112
+ actual_route = actual.delete(dest_cidr)
113
+
114
+ if actual_route
115
+ updated = walk_route(vpc_id, rt_name, dest_cidr, expected_route, actual_route) || updated
116
+ else
117
+ @driver.create_route(vpc_id, rt_name, dest_cidr, expected_route)
118
+ updated = true
119
+ end
120
+ end
121
+
122
+ actual.each do |dest_cidr, actual_route|
123
+ @driver.delete_route(vpc_id, rt_name, dest_cidr)
124
+ updated = true
125
+ end
126
+ end
127
+
128
+ def walk_route(vpc_id, rt_name, dest_cidr, expected, actual)
129
+ expected = expected.without_nil.sort_pair
130
+ actual = actual.without_nil.sort_pair
131
+ updated = false
132
+
133
+ if expected != actual
134
+ @driver.update_route(vpc_id, rt_name, dest_cidr, expected, actual)
135
+ updated = true
136
+ end
137
+
138
+ updated
139
+ end
140
+
141
+ def load_file(file)
142
+ if file.kind_of?(String)
143
+ open(file) do |f|
144
+ Mappru::DSL.parse(f.read, file)
145
+ end
146
+ elsif file.respond_to?(:read)
147
+ Mappru::DSL.parse(file.read, file.path)
148
+ else
149
+ raise TypeError, "can't convert #{file} into File"
150
+ end
151
+ end
152
+
153
+ def routes_to_hash(routes)
154
+ hash = {}
155
+
156
+ routes.map(&:dup).each do |r|
157
+ dest_cidr = r.delete(:destination_cidr_block)
158
+ hash[dest_cidr] = r
159
+ end
160
+
161
+ hash
162
+ end
163
+ end
@@ -0,0 +1,115 @@
1
+ class Mappru::Driver
2
+ include Mappru::Logger::Helper
3
+ include Mappru::Utils::Helper
4
+
5
+ def initialize(client, options = {})
6
+ @client = client
7
+ @resource = Aws::EC2::Resource.new(client: @client)
8
+ @options = options
9
+ end
10
+
11
+ def create_route_table(vpc_id, name, attrs)
12
+ log(:info, "Create Route Table `#{vpc_id}` > `#{name}`", color: :cyan)
13
+
14
+ unless @options[:dry_run]
15
+ vpc = @resource.vpc(vpc_id)
16
+ rt = vpc.create_route_table
17
+ name_tag = {key: 'Name', value: name}
18
+ rt.create_tags(tags: [name_tag])
19
+ rt_id_by_vpc_rt_name[vpc_id][name] = rt.id
20
+ end
21
+
22
+ {routes: [], subnets: []}
23
+ end
24
+
25
+ def associate_subnets(vpc_id, rt_name, subnet_ids)
26
+ log(:info, "Associate Subnets `#{vpc_id}` > `#{rt_name}`: #{subnet_ids.join(', ')}", color: :green)
27
+
28
+ unless @options[:dry_run]
29
+ subnet_ids.each do |sbnt_id|
30
+ rt_id = rt_id_by_vpc_rt_name.fetch(vpc_id).fetch(rt_name)
31
+ params = {route_table_id: rt_id, subnet_id: sbnt_id}
32
+ @client.associate_route_table(params)
33
+ end
34
+ end
35
+ end
36
+
37
+ def disassociate_subnets(vpc_id, rt_name, subnet_ids)
38
+ log(:info, "Disassociate Subnets `#{vpc_id}` > `#{rt_name}`: #{subnet_ids.join(', ')}", color: :red)
39
+
40
+ unless @options[:dry_run]
41
+ subnet_ids.each do |sbnt_id|
42
+ assoc_id = assoc_id_by_subnet.fetch(sbnt_id)
43
+ params = {association_id: assoc_id}
44
+ @client.disassociate_route_table(params)
45
+ end
46
+ end
47
+ end
48
+
49
+ def create_route(vpc_id, rt_name, dest_cidr, attrs)
50
+ log(:info, "Create Route `#{vpc_id}` > `#{rt_name}` > `#{dest_cidr}`", color: :cyan)
51
+
52
+ unless @options[:dry_run]
53
+ rt_id = rt_id_by_vpc_rt_name.fetch(vpc_id).fetch(rt_name)
54
+ params = attrs.merge(route_table_id: rt_id, destination_cidr_block: dest_cidr)
55
+ @client.create_route(params)
56
+ end
57
+ end
58
+
59
+ def delete_route(vpc_id, rt_name, dest_cidr)
60
+ log(:info, "Delete Route `#{vpc_id}` > `#{rt_name}` > `#{dest_cidr}`", color: :red)
61
+
62
+ unless @options[:dry_run]
63
+ rt_id = rt_id_by_vpc_rt_name.fetch(vpc_id).fetch(rt_name)
64
+ params = {route_table_id: rt_id, destination_cidr_block: dest_cidr}
65
+ @client.delete_route(params)
66
+ end
67
+ end
68
+
69
+ def update_route(vpc_id, rt_name, dest_cidr, route, old_route)
70
+ log(:info, "Update Route `#{vpc_id}` > `#{rt_name}` > `#{dest_cidr}`", color: :green)
71
+ log(:info, diff(old_route, route, color: @options[:color]), color: false)
72
+
73
+
74
+ unless @options[:dry_run]
75
+ rt_id = rt_id_by_vpc_rt_name.fetch(vpc_id).fetch(rt_name)
76
+ params = route.merge(route_table_id: rt_id, destination_cidr_block: dest_cidr)
77
+ @client.replace_route(params)
78
+ end
79
+ end
80
+
81
+ def rt_id_by_vpc_rt_name
82
+ return @rt_ids if @rt_ids
83
+
84
+ @rt_ids = {}
85
+ route_tables = @resource.route_tables
86
+
87
+ route_tables.each do |rt|
88
+ vpc_id = rt.vpc_id
89
+ name_tag = rt.tags.find {|i| i.key == 'Name' } || {}
90
+ name = name_tag[:value]
91
+
92
+ next unless name
93
+
94
+ @rt_ids[vpc_id] ||= {}
95
+ @rt_ids[vpc_id][name] = rt.id
96
+ end
97
+
98
+ @rt_ids
99
+ end
100
+
101
+ def assoc_id_by_subnet
102
+ return @assoc_ids if @assoc_ids
103
+
104
+ @assoc_ids = {}
105
+ associations = @resource.route_tables.map(&:associations).flat_map(&:to_a)
106
+
107
+ associations.each do |assoc|
108
+ if assoc.subnet_id
109
+ @assoc_ids[assoc.subnet_id] = assoc.id
110
+ end
111
+ end
112
+
113
+ @assoc_ids
114
+ end
115
+ end
@@ -0,0 +1,31 @@
1
+ class Mappru::DSL::Context::VPC::RouteTable
2
+ include Mappru::DSL::TemplateHelper
3
+
4
+ def initialize(context, vpc_id, name, &block)
5
+ @vpc_id = vpc_id
6
+ @name = name
7
+ @context = context.merge(vpc_id: vpc_id, name: name)
8
+ @result = {subnets: [], routes: []}
9
+ instance_eval(&block)
10
+ end
11
+
12
+ attr_reader :result
13
+
14
+ private
15
+
16
+ def subnets(*subnet_ids)
17
+ if subnet_ids.empty?
18
+ raise "VPC `#{@vpc_id}` > Route Table `#{@name}`: Subnet Id is empty"
19
+ end
20
+
21
+ @result[:subnets].concat(subnet_ids.map(&:to_s))
22
+ end
23
+
24
+ def route(attributes)
25
+ unless attributes.is_a?(Hash)
26
+ raise "VPC `#{@vpc_id}` > Route Table `#{@name}`: Invalid route #{attributes.inspect}:#{attributes.class}"
27
+ end
28
+
29
+ @result[:routes] << attributes
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ class Mappru::DSL::Context::VPC
2
+ include Mappru::DSL::TemplateHelper
3
+
4
+ def initialize(context, vpc_id, &block)
5
+ @vpc_id = vpc_id
6
+ @context = context.merge(vpc_id: vpc_id)
7
+ @result = {}
8
+ instance_eval(&block)
9
+ end
10
+
11
+ attr_reader :result
12
+
13
+ private
14
+
15
+ def route_table(name, &block)
16
+ name = name.to_s
17
+
18
+ if @result[name]
19
+ raise "Route Table `#{name}` is already defined"
20
+ end
21
+
22
+ @result[name] = Mappru::DSL::Context::VPC::RouteTable.new(@context, @vpc_id, name, &block).result
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ class Mappru::DSL::Context
2
+ include Mappru::DSL::TemplateHelper
3
+
4
+ def self.eval(dsl, path, options = {})
5
+ self.new(path, options) {
6
+ eval(dsl, binding, path)
7
+ }
8
+ end
9
+
10
+ attr_reader :result
11
+
12
+ def initialize(path, options = {}, &block)
13
+ @path = path
14
+ @options = options
15
+ @result = {}
16
+
17
+ @context = Hashie::Mash.new(
18
+ :path => path,
19
+ :options => options,
20
+ :templates => {}
21
+ )
22
+
23
+ instance_eval(&block)
24
+ end
25
+
26
+ def template(name, &block)
27
+ @context.templates[name.to_s] = block
28
+ end
29
+
30
+ private
31
+
32
+ def require(file)
33
+ rt = (file =~ %r|\A/|) ? file : File.expand_path(File.join(File.dirname(@path), file))
34
+
35
+ if File.exist?(rt)
36
+ instance_eval(File.read(rt), rt)
37
+ elsif File.exist?(rt + '.rb')
38
+ instance_eval(File.read(rt + '.rb'), rt + '.rb')
39
+ else
40
+ Kernel.require(file)
41
+ end
42
+ end
43
+
44
+ def vpc(vpc_id, &block)
45
+ vpc_id = vpc_id.to_s
46
+ @result[vpc_id] ||= {}
47
+ @result[vpc_id].update(Mappru::DSL::Context::VPC.new(@context, vpc_id, &block).result)
48
+ end
49
+ end
@@ -0,0 +1,83 @@
1
+ class Mappru::DSL::Converter
2
+ include Mappru::Utils::Helper
3
+
4
+ def self.convert(exported, options = {})
5
+ self.new(exported, options).convert
6
+ end
7
+
8
+ def initialize(exported, options = {})
9
+ @exported = exported
10
+ @options = options
11
+ end
12
+
13
+ def convert
14
+ output_vpcs(@exported)
15
+ end
16
+
17
+ private
18
+
19
+ def output_vpcs(rts_by_vpc)
20
+ vpcs = []
21
+
22
+ rts_by_vpc.sort_by(&:first).each do |vpc_id, rt_by_name|
23
+ next unless matched?(vpc_id, @options[:vpc_id])
24
+
25
+ vpcs << output_vpc(vpc_id, rt_by_name)
26
+ end
27
+
28
+ vpcs.join("\n")
29
+ end
30
+
31
+ def output_vpc(vpc_id, rt_by_name)
32
+ route_tables = output_route_tables(rt_by_name).strip
33
+
34
+ <<-EOS
35
+ vpc #{vpc_id.inspect} do
36
+ #{route_tables}
37
+ end
38
+ EOS
39
+ end
40
+
41
+ def output_route_tables(rt_by_name)
42
+ route_tables = []
43
+
44
+ rt_by_name.each do |name, rt|
45
+ route_tables << output_route_table(name, rt)
46
+ end
47
+
48
+ route_tables.join("\n")
49
+ end
50
+
51
+ def output_route_table(name, rt)
52
+ subnets = output_subnets(rt[:subnets]).strip
53
+ routes = output_routes(rt[:routes]).strip
54
+
55
+ <<-EOS
56
+ route_table #{name.inspect} do
57
+ #{subnets}
58
+ #{routes}
59
+ end
60
+ EOS
61
+ end
62
+
63
+ def output_subnets(subnets)
64
+ if subnets.empty?
65
+ '#subnets ...'
66
+ else
67
+ args = subnets.map(&:inspect).join(', ')
68
+ "subnets #{args}"
69
+ end
70
+ end
71
+
72
+ def output_routes(routes)
73
+ if routes.empty?
74
+ '#routes destination_cidr_block: ...'
75
+ else
76
+ routes.map {|r|
77
+ <<-EOS
78
+ route #{r.without_nil.modern_inspect_without_brace}
79
+ EOS
80
+ }.join
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,18 @@
1
+ module Mappru::DSL::TemplateHelper
2
+ def include_template(template_name, context = {})
3
+ tmplt = @context.templates[template_name.to_s]
4
+
5
+ unless tmplt
6
+ raise "Template `#{template_name}` is not defined"
7
+ end
8
+
9
+ context_orig = @context
10
+ @context = @context.merge(context)
11
+ instance_eval(&tmplt)
12
+ @context = context_orig
13
+ end
14
+
15
+ def context
16
+ @context
17
+ end
18
+ end
data/lib/mappru/dsl.rb ADDED
@@ -0,0 +1,11 @@
1
+ class Mappru::DSL
2
+ class << self
3
+ def convert(exported, options = {})
4
+ Mappru::DSL::Converter.convert(exported, options)
5
+ end
6
+
7
+ def parse(dsl, path, options = {})
8
+ Mappru::DSL::Context.eval(dsl, path, options).result
9
+ end
10
+ end # of class methods
11
+ end
@@ -0,0 +1,83 @@
1
+ class Mappru::Exporter
2
+ include Mappru::Logger::Helper
3
+ include Mappru::Utils::Helper
4
+
5
+ def self.export(client, options = {})
6
+ self.new(client, options).export
7
+ end
8
+
9
+ def initialize(client, options = {})
10
+ @client = client
11
+ @resource = Aws::EC2::Resource.new(client: @client)
12
+ @options = options
13
+ end
14
+
15
+ def export
16
+ result = {}
17
+ @resource
18
+
19
+ route_tables = @resource.route_tables
20
+
21
+ route_tables.each do |rt|
22
+ vpc_id = rt.vpc_id
23
+ name_tag = rt.tags.find {|i| i.key == 'Name' } || {}
24
+ name = name_tag[:value]
25
+
26
+ next unless matched?(vpc_id, @options[:vpc_id])
27
+
28
+ unless name
29
+ log(:warn, "No name Route Table can not manage: #{vpc_id}", color: :yellow)
30
+ next
31
+ end
32
+
33
+ next unless matched?(name, @options[:rt_name])
34
+
35
+ result[vpc_id] ||= {}
36
+
37
+ if result[vpc_id][name]
38
+ raise "Duplication Subnet found: #{vpc_id}: #{name}"
39
+ end
40
+
41
+ result[vpc_id][name] = export_route_table(rt)
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ private
48
+
49
+ def export_route_table(rt)
50
+ result = {
51
+ route_table_id: rt.id,
52
+ routes: [],
53
+ subnets: export_subnets(rt.associations),
54
+ }
55
+
56
+ # route -> (Array<Route>, nil)
57
+ # http://docs.aws.amazon.com/sdkforruby/api/Aws/EC2/RouteTable.html#routes-instance_method
58
+ rt.routes.reject(&:nil?).each {|route|
59
+ # Skip "local"
60
+ next if route.gateway_id == 'local'
61
+
62
+ hash = {}
63
+
64
+ [
65
+ :destination_cidr_block,
66
+ # TODO: Support Endpoint
67
+ #:destination_prefix_list_id,
68
+ :gateway_id,
69
+ :network_interface_id,
70
+ :vpc_peering_connection_id,
71
+ :nat_gateway_id,
72
+ ].each {|k| hash[k] = route.send(k) }
73
+
74
+ result[:routes] << hash
75
+ }
76
+
77
+ result
78
+ end
79
+
80
+ def export_subnets(associations)
81
+ associations.map(&:subnet_id).reject(&:nil?)
82
+ end
83
+ end
@@ -0,0 +1,29 @@
1
+ module Mappru::Ext
2
+ module HashExt
3
+ def sort_pair
4
+ new_hash = {}
5
+
6
+ self.sort_by(&:to_s).each do |k, v|
7
+ new_hash[k] = v
8
+ end
9
+
10
+ new_hash
11
+ end
12
+
13
+ def sort_pair!
14
+ self.replace(self.sort_pair)
15
+ end
16
+
17
+ def without_nil
18
+ new_hash = {}
19
+
20
+ self.each do |k, v|
21
+ new_hash[k] = v if v
22
+ end
23
+
24
+ new_hash
25
+ end
26
+ end
27
+ end
28
+
29
+ Hash.include(Mappru::Ext::HashExt)
@@ -0,0 +1,28 @@
1
+ module Mappru::Ext
2
+ module StringExt
3
+ module ClassMethods
4
+ def colorize=(value)
5
+ @colorize = value
6
+ end
7
+
8
+ def colorize
9
+ @colorize
10
+ end
11
+ end # ClassMethods
12
+
13
+ Term::ANSIColor::Attribute.named_attributes.each do |attribute|
14
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
15
+ def #{attribute.name}
16
+ if String.colorize
17
+ Term::ANSIColor.send(#{attribute.name.inspect}, self)
18
+ else
19
+ self
20
+ end
21
+ end
22
+ EOS
23
+ end
24
+ end
25
+ end
26
+
27
+ String.include(Mappru::Ext::StringExt)
28
+ String.extend(Mappru::Ext::StringExt::ClassMethods)
@@ -0,0 +1,28 @@
1
+ class Mappru::Logger < ::Logger
2
+ include Singleton
3
+
4
+ def initialize
5
+ super($stdout)
6
+
7
+ self.formatter = proc do |severity, datetime, progname, msg|
8
+ "#{msg}\n"
9
+ end
10
+
11
+ self.level = Logger::INFO
12
+ end
13
+
14
+ def set_debug(value)
15
+ self.level = value ? Logger::DEBUG : Logger::INFO
16
+ end
17
+
18
+ module Helper
19
+ def log(level, message, log_options = {})
20
+ global_option = @options || {}
21
+ message = "[#{level.to_s.upcase}] #{message}" unless level == :info
22
+ message << ' (dry-run)' if global_option[:dry_run]
23
+ message = message.send(log_options[:color]) if log_options[:color]
24
+ logger = global_option[:logger] || Mappru::Logger.instance
25
+ logger.send(level, message)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ class Mappru::Utils
2
+ module Helper
3
+ def matched?(name, regex)
4
+ if regex
5
+ regex =~ name
6
+ else
7
+ true
8
+ end
9
+ end
10
+
11
+ def diff(obj1, obj2, options = {})
12
+ diffy = Diffy::Diff.new(
13
+ obj1.pretty_inspect,
14
+ obj2.pretty_inspect,
15
+ :diff => '-u'
16
+ )
17
+
18
+ out = diffy.to_s(options[:color] ? :color : :text).gsub(/\s+\z/m, '')
19
+ out.gsub!(/^/, options[:indent]) if options[:indent]
20
+ out
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Mappru
2
+ VERSION = '0.1.0'
3
+ end
data/lib/mappru.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'aws-sdk'
2
+ require 'diffy'
3
+ require 'hash_modern_inspect'
4
+ require 'hashie'
5
+ require 'logger'
6
+ require 'pp'
7
+ require 'singleton'
8
+ require 'term/ansicolor'
9
+
10
+ require 'mappru/version'
11
+ require 'mappru/ext/hash_ext'
12
+ require 'mappru/ext/string_ext'
13
+ require 'mappru/logger'
14
+ require 'mappru/utils'
15
+
16
+ require 'mappru/client'
17
+ require 'mappru/driver'
18
+ require 'mappru/dsl'
19
+ require 'mappru/dsl/template_helper'
20
+ require 'mappru/dsl/context'
21
+ require 'mappru/dsl/context/vpc'
22
+ require 'mappru/dsl/context/vpc/route_table'
23
+ require 'mappru/dsl/converter'
24
+ require 'mappru/exporter'
data/mappru.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mappru/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'mappru'
8
+ spec.version = Mappru::VERSION
9
+ spec.authors = ['winebarrel']
10
+ spec.email = ['sgwr_dts@yahoo.co.jp']
11
+
12
+ spec.summary = %q{Mappru is a tool to manage VPC Route Table.}
13
+ spec.description = %q{Mappru is a tool to manage VPC Route Table.}
14
+ spec.homepage = 'https://github.com/winebarrel/mappru'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'aws-sdk', '~> 2.3.0'
23
+ spec.add_dependency 'diffy'
24
+ spec.add_dependency 'hash_modern_inspect'
25
+ spec.add_dependency 'hashie'
26
+ spec.add_dependency 'parallel'
27
+ spec.add_dependency 'term-ansicolor'
28
+
29
+ spec.add_development_dependency 'bundler'
30
+ spec.add_development_dependency 'rake'
31
+ spec.add_development_dependency 'rspec', '~> 3.0'
32
+ end
metadata ADDED
@@ -0,0 +1,197 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mappru
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - winebarrel
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: diffy
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hash_modern_inspect
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: hashie
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: parallel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: term-ansicolor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ description: Mappru is a tool to manage VPC Route Table.
140
+ email:
141
+ - sgwr_dts@yahoo.co.jp
142
+ executables:
143
+ - mappru
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".gitignore"
148
+ - ".rspec"
149
+ - ".travis.yml"
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - bin/console
155
+ - bin/setup
156
+ - exe/mappru
157
+ - lib/mappru.rb
158
+ - lib/mappru/client.rb
159
+ - lib/mappru/driver.rb
160
+ - lib/mappru/dsl.rb
161
+ - lib/mappru/dsl/context.rb
162
+ - lib/mappru/dsl/context/vpc.rb
163
+ - lib/mappru/dsl/context/vpc/route_table.rb
164
+ - lib/mappru/dsl/converter.rb
165
+ - lib/mappru/dsl/template_helper.rb
166
+ - lib/mappru/exporter.rb
167
+ - lib/mappru/ext/hash_ext.rb
168
+ - lib/mappru/ext/string_ext.rb
169
+ - lib/mappru/logger.rb
170
+ - lib/mappru/utils.rb
171
+ - lib/mappru/version.rb
172
+ - mappru.gemspec
173
+ homepage: https://github.com/winebarrel/mappru
174
+ licenses:
175
+ - MIT
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 2.4.5.1
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: Mappru is a tool to manage VPC Route Table.
197
+ test_files: []