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