piculet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,124 @@
1
+ # Piculet
2
+
3
+ Piculet is a tool to manage Security Group.
4
+
5
+ It defines the state of Security Group using DSL, and updates Security Group according to DSL.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'piculet'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install piculet
20
+
21
+ ## Usage
22
+
23
+ ```
24
+ shell> export AWS_ACCESS_KEY_ID='...'
25
+ shell> export AWS_SECRET_ACCESS_KEY='...'
26
+ shell> export AWS_REGION='ap-northeast-1'
27
+ shell> piculet -e -o Groupfile
28
+ shell> vi Groupfile
29
+ shell> piculet -a --dry-run
30
+ shell> piculet -a
31
+ ```
32
+
33
+ ## Routefile example
34
+
35
+ ```ruby
36
+ require 'other/groupfile'
37
+
38
+ ec2 do
39
+ security_group "default" do
40
+ description "default group"
41
+
42
+ ingress do
43
+ permission :tcp, 0..65535 do
44
+ groups(
45
+ "default"
46
+ )
47
+ end
48
+ permission :udp, 0..65535 do
49
+ groups(
50
+ "default"
51
+ )
52
+ end
53
+ permission :icmp, -1..-1 do
54
+ groups(
55
+ "default"
56
+ )
57
+ end
58
+ permission :tcp, 22..22 do
59
+ ip_ranges(
60
+ "0.0.0.0/0"
61
+ )
62
+ end
63
+ permission :udp, 60000..61000 do
64
+ ip_ranges(
65
+ "0.0.0.0/0",
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ ec2 "vpc-XXXXXXXX" do
73
+ security_group "default" do
74
+ description "default VPC security group"
75
+
76
+ ingress do
77
+ permission :tcp, 22..22 do
78
+ ip_ranges(
79
+ "0.0.0.0/0",
80
+ )
81
+ end
82
+ permission :tcp, 80..80 do
83
+ ip_ranges(
84
+ "0.0.0.0/0"
85
+ )
86
+ end
87
+ permission :udp, 60000..61000 do
88
+ ip_ranges(
89
+ "0.0.0.0/0"
90
+ )
91
+ end
92
+ permission :any do
93
+ groups(
94
+ "any_other_group",
95
+ "default"
96
+ )
97
+ end
98
+ end
99
+
100
+ egress do
101
+ permission :any do
102
+ ip_ranges(
103
+ "0.0.0.0/0"
104
+ )
105
+ end
106
+ end
107
+ end
108
+
109
+ security_group "any_other_group" do
110
+ description "any_other_group"
111
+
112
+ egress do
113
+ permission :any do
114
+ ip_ranges(
115
+ "0.0.0.0/0"
116
+ )
117
+ end
118
+ end
119
+ end
120
+ end
121
+ ```
122
+
123
+ ## Link
124
+ * [RubyGems.org site](http://rubygems.org/gems/piculet)
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path("#{File.dirname __FILE__}/../lib")
3
+ require 'rubygems'
4
+ require 'piculet'
5
+ require 'optparse'
6
+
7
+ mode = nil
8
+ file = 'Groupfile'
9
+ output_file = '-'
10
+ split = false
11
+
12
+ options = {
13
+ :dry_run => false,
14
+ :color => true,
15
+ :debug => false,
16
+ }
17
+
18
+ ARGV.options do |opt|
19
+ begin
20
+ access_key = nil
21
+ secret_key = nil
22
+
23
+ opt.on('-k', '--access-key ACCESS_KEY') {|v| access_key = v }
24
+ opt.on('-s', '--secret-key SECRET_KEY') {|v| secret_key = v }
25
+ opt.on('-a', '--apply') {|v| mode = :apply }
26
+ opt.on('-f', '--file FILE') {|v| file = v }
27
+ opt.on('', '--dry-run') {|v| options[:dry_run] = true }
28
+ opt.on('-e', '--export') {|v| mode = :export }
29
+ opt.on('-o', '--output FILE') {|v| output_file = v }
30
+ opt.on('', '--split') {|v| split = true }
31
+ opt.on('-t', '--test') {|v| mode = :test }
32
+ opt.on('' , '--no-color') { options[:color] = false }
33
+ opt.on('' , '--debug') { options[:debug] = true }
34
+ opt.parse!
35
+
36
+ if access_key and secret_key
37
+ AWS.config({
38
+ :access_key_id => access_key,
39
+ :secret_access_key => secret_key,
40
+ })
41
+ elsif (access_key and !secret_key) or (!access_key and secret_key) or mode.nil?
42
+ puts opt.help
43
+ exit 1
44
+ end
45
+ rescue => e
46
+ $stderr.puts e
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ String.colorize = options[:color]
52
+
53
+ if options[:debug]
54
+ AWS.config({
55
+ :http_wire_trace => true,
56
+ :logger => Piculet::Logger.instance,
57
+ })
58
+ end
59
+
60
+ begin
61
+ logger = Piculet::Logger.instance
62
+ logger.set_debug(options[:debug])
63
+ client = Piculet::Client.new(options)
64
+
65
+ case mode
66
+ when :export
67
+ if split
68
+ logger.info('Export SecurityGroup')
69
+
70
+ output_file = 'Groupfile' if output_file == '-'
71
+ requires = []
72
+
73
+ client.export do |exported, converter|
74
+ exported.each do |vpc, security_groups|
75
+ group_file = File.join(File.dirname(output_file), "#{vpc || :classic}.group")
76
+ requires << group_file
77
+
78
+ logger.info(" write `#{group_file}`")
79
+
80
+ open(group_file, 'wb') do |f|
81
+ f.puts converter.call(vpc => security_groups)
82
+ end
83
+ end
84
+ end
85
+
86
+ logger.info(" write `#{output_file}`")
87
+
88
+ open(output_file, 'wb') do |f|
89
+ requires.each do |group_file|
90
+ f.puts "require '#{File.basename group_file}'"
91
+ end
92
+ end
93
+ else
94
+ if output_file == '-'
95
+ logger.info('# Export SecurityGroup')
96
+ puts client.export
97
+ else
98
+ logger.info("Export SecurityGroup to `#{output_file}`")
99
+ open(output_file, 'wb') {|f| f.puts client.export }
100
+ end
101
+ end
102
+ when :apply
103
+ unless File.exist?(file)
104
+ raise "No Groupfile found (looking for: #{file})"
105
+ end
106
+
107
+ msg = "Apply `#{file}` to SecurityGroup"
108
+ msg << ' (dry-run)' if options[:dry_run]
109
+ logger.info(msg)
110
+
111
+ updated = client.apply(file)
112
+
113
+ logger.info('No change'.intense_blue) unless updated
114
+ else
115
+ raise 'must not happen'
116
+ end
117
+ rescue => e
118
+ if options[:debug]
119
+ raise e
120
+ else
121
+ $stderr.puts e
122
+ exit 1
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ require 'piculet/version'
2
+ require 'piculet/client'
3
+ require 'piculet/logger'
@@ -0,0 +1,148 @@
1
+ require 'aws-sdk'
2
+ require 'piculet/dsl'
3
+ require 'piculet/exporter'
4
+ require 'piculet/ext/ec2-owner-id-ext'
5
+ require 'piculet/wrapper/ec2-wrapper'
6
+ require 'piculet/logger'
7
+
8
+ module Piculet
9
+ class Client
10
+ include Logger::ClientHelper
11
+
12
+ def initialize(options = {})
13
+ @options = OpenStruct.new(options)
14
+ @options.ec2 = AWS::EC2.new
15
+ end
16
+
17
+ def apply(file)
18
+ @options.ec2.owner_id
19
+ AWS.memoize { walk(file) }
20
+ end
21
+
22
+ def export
23
+ exported = AWS.memoize { Exporter.export(@options.ec2) }
24
+
25
+ if block_given?
26
+ converter = proc do |src|
27
+ DSL.convert(src, @options.ec2.owner_id)
28
+ end
29
+
30
+ yield(exported, converter)
31
+ else
32
+ DSL.convert(exported, @options.ec2.owner_id)
33
+ end
34
+ end
35
+
36
+ private
37
+ def load_file(file)
38
+ if file.kind_of?(String)
39
+ open(file) do |f|
40
+ DSL.define(f.read, file).result
41
+ end
42
+ elsif file.respond_to?(:read)
43
+ DSL.define(file.read, file.path).result
44
+ else
45
+ raise TypeError, "can't convert #{file} into File"
46
+ end
47
+ end
48
+
49
+ def walk(file)
50
+ dsl = load_file(file)
51
+
52
+ dsl_ec2s = dsl.ec2s
53
+ ec2 = EC2Wrapper.new(@options.ec2, @options)
54
+
55
+ aws_ec2s = collect_to_hash(ec2.security_groups, :has_many => true) do |item|
56
+ item.vpc? ? item.vpc_id : nil
57
+ end
58
+
59
+ dsl_ec2s.each do |vpc, ec2_dsl|
60
+ ec2_aws = aws_ec2s[vpc]
61
+
62
+ if ec2_aws
63
+ walk_ec2(vpc, ec2_dsl, ec2_aws, ec2.security_groups)
64
+ else
65
+ log(:warn, "EC2 `#{vpc || :classic}` is not found", :yellow)
66
+ end
67
+ end
68
+
69
+ ec2.updated?
70
+ end
71
+
72
+ def walk_ec2(vpc, ec2_dsl, ec2_aws, collection_api)
73
+ sg_list_dsl = collect_to_hash(ec2_dsl.security_groups, :name)
74
+ sg_list_aws = collect_to_hash(ec2_aws, :name)
75
+
76
+ sg_list_dsl.each do |key, sg_dsl|
77
+ name = key[0]
78
+ sg_aws = sg_list_aws.delete(key)
79
+
80
+ unless sg_aws
81
+ sg_aws = collection_api.create(name, :vpc => vpc, :description => sg_dsl.description)
82
+ end
83
+
84
+ walk_security_group(sg_dsl, sg_aws)
85
+ end
86
+
87
+ sg_list_aws.each do |key, sg_aws|
88
+ sg_aws.delete
89
+ end
90
+ end
91
+
92
+ def walk_security_group(security_group_dsl, security_group_aws)
93
+ unless security_group_aws.eql?(security_group_dsl)
94
+ security_group_aws.update(security_group_dsl)
95
+ end
96
+
97
+ walk_permissions(
98
+ security_group_dsl.ingress,
99
+ security_group_aws.ingress_ip_permissions)
100
+
101
+ if security_group_aws.vpc?
102
+ walk_permissions(
103
+ security_group_dsl.egress,
104
+ security_group_aws.egress_ip_permissions)
105
+ end
106
+ end
107
+
108
+ def walk_permissions(permissions_dsl, permissions_aws)
109
+ perm_list_dsl = collect_to_hash(permissions_dsl, :protocol, :port_range)
110
+ perm_list_aws = collect_to_hash(permissions_aws, :protocol, :port_range)
111
+
112
+ perm_list_dsl.each do |key, perm_dsl|
113
+ protocol, port_range = key
114
+ perm_aws = perm_list_aws.delete(key)
115
+
116
+ if perm_aws
117
+ unless perm_aws.eql?(perm_dsl)
118
+ perm_aws.update(perm_dsl)
119
+ end
120
+ else
121
+ permissions_aws.create(protocol, port_range, perm_dsl)
122
+ end
123
+ end
124
+
125
+ perm_list_aws.each do |key, perm_aws|
126
+ perm_aws.delete
127
+ end
128
+ end
129
+
130
+ def collect_to_hash(collection, *key_attrs)
131
+ options = key_attrs.last.kind_of?(Hash) ? key_attrs.pop : {}
132
+ hash = {}
133
+
134
+ collection.each do |item|
135
+ key = block_given? ? yield(item) : key_attrs.map {|k| item.send(k) }
136
+
137
+ if options[:has_many]
138
+ hash[key] ||= []
139
+ hash[key] << item
140
+ else
141
+ hash[key] = item
142
+ end
143
+ end
144
+
145
+ return hash
146
+ end
147
+ end # Client
148
+ end # Piculet
@@ -0,0 +1,44 @@
1
+ require 'ostruct'
2
+ require 'piculet/dsl/ec2'
3
+ require 'piculet/dsl/converter'
4
+
5
+ module Piculet
6
+ class DSL
7
+ class << self
8
+ def define(source, path)
9
+ self.new(path) do
10
+ eval(source, binding)
11
+ end
12
+ end
13
+
14
+ def convert(exported, owner_id)
15
+ Converter.convert(exported, owner_id)
16
+ end
17
+ end # of class methods
18
+
19
+ attr_reader :result
20
+
21
+ def initialize(path, &block)
22
+ @path = path
23
+ @result = OpenStruct.new(:ec2s => {})
24
+ instance_eval(&block)
25
+ end
26
+
27
+ private
28
+ def require(file)
29
+ groupfile = File.expand_path(File.join(File.dirname(@path), file))
30
+
31
+ if File.exist?(groupfile)
32
+ instance_eval(File.read(groupfile))
33
+ elsif File.exist?(groupfile + '.rb')
34
+ instance_eval(File.read(groupfile + '.rb'))
35
+ else
36
+ Kernel.require(file)
37
+ end
38
+ end
39
+
40
+ def ec2(vpc = nil, &block)
41
+ @result.ec2s[vpc] = EC2.new(vpc, &block).result
42
+ end
43
+ end # DSL
44
+ end # Piculet
@@ -0,0 +1,120 @@
1
+ module Piculet
2
+ class DSL
3
+ class Converter
4
+ class << self
5
+ def convert(exported, owner_id)
6
+ self.new(exported, owner_id).convert
7
+ end
8
+ end # of class methods
9
+
10
+ def initialize(exported, owner_id)
11
+ @exported = exported
12
+ @owner_id = owner_id
13
+ end
14
+
15
+ def convert
16
+ @exported.each.map {|vpc, security_groups|
17
+ output_ec2(vpc, security_groups)
18
+ }.join("\n")
19
+ end
20
+
21
+ private
22
+ def output_ec2(vpc, security_groups)
23
+ vpc = vpc ? vpc.inspect + ' ' : ''
24
+ security_groups = security_groups.map {|sg_id, sg|
25
+ output_security_group(sg_id, sg)
26
+ }.join("\n").strip
27
+
28
+ <<-EOS
29
+ ec2 #{vpc}do
30
+ #{security_groups}
31
+ end
32
+ EOS
33
+ end
34
+
35
+ def output_security_group(security_group_id, security_group)
36
+ name = security_group[:name].inspect
37
+ description = security_group[:description].inspect
38
+
39
+ ingress = security_group.fetch(:ingress, [])
40
+ egress = security_group.fetch(:egress, [])
41
+
42
+ ingress_egress = [
43
+ output_permissions(:ingress, ingress),
44
+ output_permissions(:egress, egress),
45
+ ].select {|i| i }
46
+
47
+ ingress_egress = ingress_egress.empty? ? '' : "\n\n " + ingress_egress.join("\n").strip
48
+
49
+ <<-EOS
50
+ security_group #{name} do
51
+ description #{description}#{
52
+ ingress_egress}
53
+ end
54
+ EOS
55
+ end
56
+
57
+ def output_permissions(direction, permissions)
58
+ return nil if permissions.empty?
59
+ permissions = permissions.map {|i| output_perm(i) }.join.strip
60
+
61
+ <<-EOS
62
+ #{direction} do
63
+ #{permissions}
64
+ end
65
+ EOS
66
+ end
67
+
68
+ def output_perm(permission)
69
+ protocol = permission[:protocol]
70
+ port_range = permission[:port_range]
71
+ args = [protocol, port_range].select {|i| i }.map {|i| i.inspect }.join(', ') + ' '
72
+
73
+ ip_ranges = permission.fetch(:ip_ranges, [])
74
+ groups = permission.fetch(:groups, [])
75
+
76
+ ip_ranges_groups = [
77
+ output_ip_ranges(ip_ranges),
78
+ output_groups(groups),
79
+ ].select {|i| i }.join.strip
80
+
81
+ ip_ranges_groups.insert(0, "\n ") unless ip_ranges_groups.empty?
82
+
83
+ <<-EOS
84
+ permission #{args}do#{
85
+ ip_ranges_groups}
86
+ end
87
+ EOS
88
+ end
89
+
90
+ def output_ip_ranges(ip_ranges)
91
+ return nil if ip_ranges.empty?
92
+ ip_ranges = ip_ranges.map {|i| i.inspect }.join(",\n ")
93
+
94
+ <<-EOS
95
+ ip_ranges(
96
+ #{ip_ranges}
97
+ )
98
+ EOS
99
+ end
100
+
101
+ def output_groups(groups)
102
+ return nil if groups.empty?
103
+
104
+ groups = groups.map {|i|
105
+ name_or_id = i[:name] || i[:id]
106
+ owner_id = i[:owner_id]
107
+
108
+ arg = @owner_id == owner_id ? name_or_id : [owner_id, i[:id]]
109
+ arg.inspect
110
+ }.join(",\n ")
111
+
112
+ <<-EOS
113
+ groups(
114
+ #{groups}
115
+ )
116
+ EOS
117
+ end
118
+ end # Converter
119
+ end # DSL
120
+ end # Piculet