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.
- data/README.md +124 -0
- data/bin/piculet +124 -0
- data/lib/piculet.rb +3 -0
- data/lib/piculet/client.rb +148 -0
- data/lib/piculet/dsl.rb +44 -0
- data/lib/piculet/dsl/converter.rb +120 -0
- data/lib/piculet/dsl/ec2.rb +30 -0
- data/lib/piculet/dsl/permission.rb +64 -0
- data/lib/piculet/dsl/permissions.rb +47 -0
- data/lib/piculet/dsl/security-group.rb +48 -0
- data/lib/piculet/exporter.rb +58 -0
- data/lib/piculet/ext/ec2-owner-id-ext.rb +88 -0
- data/lib/piculet/ext/ip-permission-collection-ext.rb +45 -0
- data/lib/piculet/ext/string-ext.rb +27 -0
- data/lib/piculet/logger.rb +33 -0
- data/lib/piculet/version.rb +5 -0
- data/lib/piculet/wrapper/ec2-wrapper.rb +18 -0
- data/lib/piculet/wrapper/permission-collection.rb +134 -0
- data/lib/piculet/wrapper/permission.rb +92 -0
- data/lib/piculet/wrapper/security-group-collection.rb +36 -0
- data/lib/piculet/wrapper/security-group.rb +56 -0
- metadata +149 -0
data/README.md
ADDED
@@ -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)
|
data/bin/piculet
ADDED
@@ -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
|
data/lib/piculet.rb
ADDED
@@ -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
|
data/lib/piculet/dsl.rb
ADDED
@@ -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
|