holepunch 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/holepunch +26 -0
- data/lib/holepunch.rb +48 -0
- data/lib/holepunch/cli.rb +109 -0
- data/lib/holepunch/definition.rb +98 -0
- data/lib/holepunch/dsl.rb +99 -0
- data/lib/holepunch/ec2.rb +160 -0
- data/lib/holepunch/logger.rb +54 -0
- data/lib/holepunch/version.rb +23 -0
- metadata +223 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fe47a0599c4e1b19b2d568547aeffe9da1f6fe7c
|
4
|
+
data.tar.gz: 587e038f58741e6f9964cc3de7bbfe633408d5e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91dcdcd8826236fe71ec2dfe9a1267797418f832611960912e95401a2c36478b1cdb2cb650b5467d2f6760859eeaf1beec58d94aedd6b638eca0b746b791facc
|
7
|
+
data.tar.gz: 11b27c316a5d8c62c69055c9b12271b33e05b57535c541518c954b9e2bfab5482e679bd13788bca94cf154c25e3e4b41dccb1381d106243f169319028f553ef3
|
data/bin/holepunch
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
# this software and associated documentation files (the "Software"), to deal in
|
7
|
+
# the Software without restriction, including without limitation the rights to
|
8
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
# subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# 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, FITNESS
|
17
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
#
|
22
|
+
lib = File.expand_path('../../lib', __FILE__)
|
23
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
24
|
+
require 'holepunch/cli'
|
25
|
+
|
26
|
+
HolePunch::Cli.new.execute!(ARGV.dup)
|
data/lib/holepunch.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require 'aws-sdk'
|
22
|
+
require 'holepunch/definition'
|
23
|
+
require 'holepunch/dsl'
|
24
|
+
require 'holepunch/ec2'
|
25
|
+
require 'holepunch/logger'
|
26
|
+
require 'holepunch/version'
|
27
|
+
|
28
|
+
module HolePunch
|
29
|
+
class HolePunchError < StandardError; end
|
30
|
+
|
31
|
+
class EnvNotDefinedError < HolePunchError; end
|
32
|
+
class GroupError < HolePunchError; end
|
33
|
+
class GroupDoesNotExistError < HolePunchError; end
|
34
|
+
class SecurityGroupsFileNotFoundError < HolePunchError; end
|
35
|
+
class SecurityGroupsFileError < HolePunchError; end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
def cidr?(value)
|
39
|
+
value.to_s =~ /\d+\.\d+\.\d+\.\d+\/\d+/
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_file(file)
|
43
|
+
content = File.open(file, 'rb') do |io|
|
44
|
+
io.read
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require 'holepunch'
|
22
|
+
require 'optparse'
|
23
|
+
|
24
|
+
module HolePunch
|
25
|
+
class Options < Struct.new(
|
26
|
+
:aws_access_key_id,
|
27
|
+
:aws_region,
|
28
|
+
:aws_secret_access_key,
|
29
|
+
:env,
|
30
|
+
:filename,
|
31
|
+
:verbose
|
32
|
+
); end
|
33
|
+
|
34
|
+
class Cli
|
35
|
+
def initialize
|
36
|
+
Logger.output = LoggerOutputStdio.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute!(args)
|
40
|
+
opts = parse_opts(args)
|
41
|
+
Logger.verbose = opts.verbose
|
42
|
+
|
43
|
+
definition = Definition.build(opts.filename, opts.env)
|
44
|
+
|
45
|
+
ec2 = EC2.new({
|
46
|
+
access_key_id: opts.aws_access_key_id,
|
47
|
+
secret_access_key: opts.aws_secret_access_key,
|
48
|
+
region: opts.aws_region,
|
49
|
+
})
|
50
|
+
ec2.apply(definition)
|
51
|
+
|
52
|
+
rescue EnvNotDefinedError => e
|
53
|
+
Logger.fatal('You have security groups that use an environment, but you did not specify one. See --help')
|
54
|
+
rescue HolePunchError => e
|
55
|
+
Logger.fatal(e.message)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def parse_opts(args)
|
60
|
+
opts = Options.new
|
61
|
+
opts.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
|
62
|
+
opts.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
|
63
|
+
opts.aws_region = ENV['AWS_REGION']
|
64
|
+
opts.env = nil
|
65
|
+
opts.filename = "#{Dir.pwd}/SecurityGroups"
|
66
|
+
opts.verbose = false
|
67
|
+
|
68
|
+
OptionParser.new(<<-EOS.gsub(/^ {10}/, '')
|
69
|
+
Usage: holepunch [options]
|
70
|
+
|
71
|
+
Options:
|
72
|
+
EOS
|
73
|
+
) do |parser|
|
74
|
+
parser.on('-A', '--aws-access-key KEY', String, 'Your AWS Access Key ID') do |value|
|
75
|
+
opts.aws_access_key_id = value
|
76
|
+
end
|
77
|
+
parser.on('-e', '--env ENV', String, 'Set the environment') do |value|
|
78
|
+
opts.env = value
|
79
|
+
end
|
80
|
+
parser.on('-f', '--file FILENAME', String, 'The location of the SecurityGroups file to use') do |value|
|
81
|
+
opts.filename = value
|
82
|
+
end
|
83
|
+
parser.on('-K', '--aws-secret-access-key SECRET', String, 'Your AWS API Secret Access Key') do |value|
|
84
|
+
opts.aws_secret_access_key = value
|
85
|
+
end
|
86
|
+
parser.on('-r', '--region REGION', String, 'Your AWS region') do |v|
|
87
|
+
opts.aws_region = v
|
88
|
+
end
|
89
|
+
parser.on('-v', '--verbose', 'verbose output') do |v|
|
90
|
+
opts.verbose = v
|
91
|
+
end
|
92
|
+
parser.on('-V', '--version', 'display the version and exit') do
|
93
|
+
puts VERSION
|
94
|
+
exit
|
95
|
+
end
|
96
|
+
parser.on_tail('-h', '--help', 'show this message') do
|
97
|
+
puts parser
|
98
|
+
exit
|
99
|
+
end
|
100
|
+
end.parse!(args)
|
101
|
+
|
102
|
+
Logger.fatal("AWS Access Key ID not defined. Use --aws-access-key or AWS_ACCESS_KEY_ID") if opts.aws_access_key_id.nil?
|
103
|
+
Logger.fatal("AWS Secret Access Key not defined. Use --aws-secret-access-key or AWS_SECRET_ACCESS_KEY") if opts.aws_secret_access_key.nil?
|
104
|
+
Logger.fatal("AWS Region not defined. Use --aws-region or AWS_REGION") if opts.aws_region.nil?
|
105
|
+
|
106
|
+
opts
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
module HolePunch
|
22
|
+
class SecurityGroup
|
23
|
+
attr_accessor :id, :desc, :dependency, :ingresses
|
24
|
+
|
25
|
+
def initialize(id, opts = {})
|
26
|
+
opts = {
|
27
|
+
dependency: false
|
28
|
+
}.merge(opts)
|
29
|
+
|
30
|
+
@id = id
|
31
|
+
@desc = id
|
32
|
+
@dependency = opts[:dependency]
|
33
|
+
@ingresses = []
|
34
|
+
end
|
35
|
+
|
36
|
+
def include_ingress?(type, ports, source)
|
37
|
+
ports = ports.first if ports.is_a?(Range) and ports.size == 1
|
38
|
+
|
39
|
+
ingresses.any? do |ingress|
|
40
|
+
ingress.type == type && ingress.ports == ports && ingress.sources.include?(source)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Permission < Struct.new(:type, :ports, :sources)
|
46
|
+
def icmp?
|
47
|
+
type == :icmp
|
48
|
+
end
|
49
|
+
|
50
|
+
def tcp?
|
51
|
+
type == :tcp
|
52
|
+
end
|
53
|
+
|
54
|
+
def udp?
|
55
|
+
type == :udp
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Definition
|
60
|
+
attr_reader :env
|
61
|
+
attr_reader :groups
|
62
|
+
|
63
|
+
class << self
|
64
|
+
def build(file, env)
|
65
|
+
filename = Pathname.new(file).expand_path
|
66
|
+
unless filename.file?
|
67
|
+
raise SecurityGroupsFileNotFoundError, "#{filename} not found"
|
68
|
+
end
|
69
|
+
|
70
|
+
DSL.evaluate(file, env)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize(env = nil)
|
75
|
+
@env = env
|
76
|
+
@groups = {}
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_group(group)
|
80
|
+
raise DuplicateGroupError, "another group already exists with id #{id}" if groups.include?(group.id)
|
81
|
+
groups[group.id] = group
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate!
|
85
|
+
# verify group references are defined
|
86
|
+
groups.each do |id, group|
|
87
|
+
group.ingresses.each do |ingress|
|
88
|
+
ingress.sources.each do |source|
|
89
|
+
next if HolePunch.cidr?(source)
|
90
|
+
unless groups.include?(source)
|
91
|
+
raise GroupError, "group '#{source}' referenced by group '#{id}' does not exist"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require 'pathname'
|
22
|
+
|
23
|
+
module HolePunch
|
24
|
+
class DSL
|
25
|
+
attr_reader :groups
|
26
|
+
|
27
|
+
def self.evaluate(filename, env = nil)
|
28
|
+
DSL.new(env).eval_dsl(filename)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(env)
|
32
|
+
@definition = Definition.new(env)
|
33
|
+
@group = nil
|
34
|
+
@groups = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def eval_dsl(filename)
|
38
|
+
instance_eval(HolePunch.read_file(filename.to_s), filename.to_s, 1)
|
39
|
+
@definition.validate!
|
40
|
+
@definition
|
41
|
+
rescue SyntaxError => e
|
42
|
+
raise SecurityGroupsFileError, "SecurityGroups syntax error #{e.message.gsub("#{filename.to_s}:", 'on line ')}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def env
|
46
|
+
raise EnvNotDefinedError, 'env not defined' if @definition.env.nil?
|
47
|
+
@definition.env
|
48
|
+
end
|
49
|
+
|
50
|
+
def depends(id)
|
51
|
+
id = id.to_s
|
52
|
+
raise GroupError, "duplicate group id #{id}" if @definition.groups.include?(id)
|
53
|
+
raise HolePunchSyntaxError, "dependency group #{id} cannot have a block" if block_given?
|
54
|
+
@group = SecurityGroup.new(id, dependency: true)
|
55
|
+
@definition.add_group(@group)
|
56
|
+
yield if block_given?
|
57
|
+
ensure
|
58
|
+
@group = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def group(id, &block)
|
62
|
+
id = id.to_s
|
63
|
+
raise GroupError, "duplicate group id #{id}" if @definition.groups.include?(id)
|
64
|
+
@group = SecurityGroup.new(id, dependency: false)
|
65
|
+
@definition.add_group(@group)
|
66
|
+
yield if block_given?
|
67
|
+
ensure
|
68
|
+
@group = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def desc(str)
|
72
|
+
raise HolePunchSyntaxError, 'desc must be used inside a group' if @group.nil?
|
73
|
+
raise HolePunchSyntaxError, 'desc cannot be used in a dependency group (the group is expected to be already defined elsewhere)' if @group.dependency
|
74
|
+
@group.desc = str
|
75
|
+
end
|
76
|
+
|
77
|
+
def icmp(*sources)
|
78
|
+
raise HolePunchSyntaxError, 'ping/icmp must be used inside a group' if @group.nil?
|
79
|
+
raise HolePunchSyntaxError, 'ping/icmp cannot be used in a dependency group (the group is expected to be already defined elsewhere)' if @group.dependency
|
80
|
+
sources << '0.0.0.0/0' if sources.empty?
|
81
|
+
@group.ingresses << Permission.new(:icmp, nil, sources.flatten)
|
82
|
+
end
|
83
|
+
alias_method :ping, :icmp
|
84
|
+
|
85
|
+
def tcp(ports, *sources)
|
86
|
+
raise HolePunchSyntaxError, 'tcp must be used inside a group' if @group.nil?
|
87
|
+
raise HolePunchSyntaxError, 'tcp cannot be used in a dependency group (the group is expected to be already defined elsewhere)' if @group.dependency
|
88
|
+
sources << '0.0.0.0/0' if sources.empty?
|
89
|
+
@group.ingresses << Permission.new(:tcp, ports, sources.flatten)
|
90
|
+
end
|
91
|
+
|
92
|
+
def udp(ports, *sources)
|
93
|
+
raise HolePunchSyntaxError, 'udp must be used inside a group' if @group.nil?
|
94
|
+
raise HolePunchSyntaxError, 'udp cannot be used in a dependency group (the group is expected to be already defined elsewhere)' if @group.dependency
|
95
|
+
sources << '0.0.0.0/0' if sources.empty?
|
96
|
+
@group.ingresses << Permission.new(:udp, ports, sources.flatten)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require 'shellwords'
|
22
|
+
|
23
|
+
module HolePunch
|
24
|
+
class EC2
|
25
|
+
attr_reader :ec2
|
26
|
+
attr_reader :region
|
27
|
+
|
28
|
+
def initialize(opts = {})
|
29
|
+
opts = {
|
30
|
+
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
|
31
|
+
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
|
32
|
+
region: ENV['AWS_REGION'],
|
33
|
+
}.merge(opts)
|
34
|
+
|
35
|
+
AWS.config(opts)
|
36
|
+
|
37
|
+
@ec2 = AWS::EC2.new
|
38
|
+
@region = @ec2.regions[opts[:region]]
|
39
|
+
end
|
40
|
+
|
41
|
+
def apply(definition)
|
42
|
+
if definition.env.nil?
|
43
|
+
Logger.log("Creating security groups in '#{@region.name}' region")
|
44
|
+
else
|
45
|
+
Logger.log("Creating security groups for '#{definition.env}' environment in '#{@region.name}' region")
|
46
|
+
end
|
47
|
+
|
48
|
+
# get the security group data from the AWS servers
|
49
|
+
fetch!
|
50
|
+
|
51
|
+
# ensure dependency groups exist
|
52
|
+
definition.groups.select { |id, group| group.dependency }.each do |id, group|
|
53
|
+
unless exists?(id)
|
54
|
+
raise GroupDoesNotExistError, "Dependent security group '#{id}' does not exist"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# find/create the groups
|
59
|
+
ec2_groups = {}
|
60
|
+
definition.groups.each do |id, group|
|
61
|
+
ec2_group = find(id)
|
62
|
+
if ec2_group.nil?
|
63
|
+
Logger.log(:create, id)
|
64
|
+
ec2_group = create(id, group.desc)
|
65
|
+
end
|
66
|
+
ec2_groups[id] = ec2_group
|
67
|
+
end
|
68
|
+
|
69
|
+
definition.groups.each do |id, group|
|
70
|
+
next if group.dependency
|
71
|
+
ec2_group = ec2_groups[id]
|
72
|
+
|
73
|
+
# revoke existing ingresses no longer desired
|
74
|
+
ec2_group.ingress_ip_permissions.each do |ec2_perm|
|
75
|
+
revoke_sources = []
|
76
|
+
ec2_perm.groups.each do |source|
|
77
|
+
unless group.include_ingress?(ec2_perm.protocol, ec2_perm.port_range, source.name)
|
78
|
+
revoke_sources << source
|
79
|
+
end
|
80
|
+
end
|
81
|
+
ec2_perm.ip_ranges.each do |source|
|
82
|
+
unless group.include_ingress?(ec2_perm.protocol, ec2_perm.port_range, source)
|
83
|
+
revoke_sources << source
|
84
|
+
end
|
85
|
+
end
|
86
|
+
unless revoke_sources.empty?
|
87
|
+
Logger.log("revoke #{ec2_perm.protocol}", "#{id} #{sources_list_to_s(revoke_sources)}")
|
88
|
+
ec2_group.revoke_ingress(ec2_perm.protocol, ec2_perm.port_range, *revoke_sources)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# add new ingresses
|
93
|
+
group.ingresses.each do |perm|
|
94
|
+
new_sources = []
|
95
|
+
perm.sources.each do |source|
|
96
|
+
if HolePunch.cidr?(source)
|
97
|
+
unless group_has_ingress(ec2_group, perm.type, perm.ports, source)
|
98
|
+
new_sources << source
|
99
|
+
end
|
100
|
+
else
|
101
|
+
ec2_source_group = ec2_groups[source]
|
102
|
+
if ec2_source_group.nil?
|
103
|
+
raise GroupDoesNotExistError, "unknown security group '#{source}"
|
104
|
+
end
|
105
|
+
unless group_has_ingress(ec2_group, perm.type, perm.ports, ec2_source_group)
|
106
|
+
new_sources << ec2_source_group
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
unless new_sources.empty?
|
111
|
+
Logger.log(perm.type, "#{id} #{sources_list_to_s(new_sources)}")
|
112
|
+
ec2_group.authorize_ingress(perm.type, perm.ports, *new_sources)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
private
|
120
|
+
def fetch!
|
121
|
+
@groups = @region.security_groups.to_a
|
122
|
+
end
|
123
|
+
|
124
|
+
def create(name, description)
|
125
|
+
group = @region.security_groups.create(name, description: description)
|
126
|
+
@groups << group
|
127
|
+
group
|
128
|
+
end
|
129
|
+
|
130
|
+
def find(name)
|
131
|
+
@groups.detect { |group| group.name == name }
|
132
|
+
end
|
133
|
+
|
134
|
+
def exists?(name)
|
135
|
+
!find(name).nil?
|
136
|
+
end
|
137
|
+
|
138
|
+
def group_has_ingress(group, protocol, ports, source)
|
139
|
+
ports = ports..ports unless ports.is_a?(Range)
|
140
|
+
is_cidr = HolePunch.cidr?(source)
|
141
|
+
group.ingress_ip_permissions.any? do |perm|
|
142
|
+
if is_cidr
|
143
|
+
perm.protocol == protocol && perm.port_range == ports && perm.ip_ranges.include?(source)
|
144
|
+
else
|
145
|
+
perm.protocol == protocol && perm.port_range == ports && perm.groups.include?(source)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def sources_list_to_s(sources)
|
151
|
+
sources.map do |source|
|
152
|
+
if source.is_a?(AWS::EC2::SecurityGroup)
|
153
|
+
source.name
|
154
|
+
else
|
155
|
+
source.to_s
|
156
|
+
end.shellescape
|
157
|
+
end.sort.join(' ')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
module HolePunch
|
22
|
+
class LoggerOutputStdio
|
23
|
+
def log(msg)
|
24
|
+
$stdout.puts(msg)
|
25
|
+
end
|
26
|
+
|
27
|
+
def fatal(msg)
|
28
|
+
$stderr.puts(msg)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Logger
|
33
|
+
class << self
|
34
|
+
attr_accessor :verbose
|
35
|
+
attr_accessor :output
|
36
|
+
|
37
|
+
def log(prefix, msg = nil)
|
38
|
+
return unless verbose
|
39
|
+
return if output.nil?
|
40
|
+
if msg.nil?
|
41
|
+
output.log prefix
|
42
|
+
else
|
43
|
+
output.log "#{prefix.to_s.rjust(12)} #{msg}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def fatal(msg, exit_code = 1)
|
48
|
+
return if output.nil?
|
49
|
+
output.fatal("ERROR: #{msg}")
|
50
|
+
exit(exit_code)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2014 Undead Labs, LLC
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
module HolePunch
|
22
|
+
VERSION = '1.0.0'
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: holepunch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Scott
|
8
|
+
- Pat Wyatt
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-06-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: aws-sdk
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.32'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.32'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.8.7
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.8.7
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rspec
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '3.0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.0'
|
56
|
+
description: |
|
57
|
+
# holepunch
|
58
|
+
[![Build Status](https://travis-ci.org/undeadlabs/holepunch.svg?branch=master)](https://travis-ci.org/undeadlabs/holepunch)
|
59
|
+
|
60
|
+
Holepunch manages AWS EC2 security groups in a declarative way through a DSL.
|
61
|
+
|
62
|
+
## Requirements
|
63
|
+
|
64
|
+
- Ruby 1.9.3 or newer.
|
65
|
+
|
66
|
+
## Installation
|
67
|
+
|
68
|
+
```bash
|
69
|
+
gem install holepunch
|
70
|
+
```
|
71
|
+
|
72
|
+
or in your Gemfile
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
gem 'holepunch'
|
76
|
+
```
|
77
|
+
|
78
|
+
## Basic Configuration
|
79
|
+
|
80
|
+
You need to provide your AWS security credentials and a region. These can be
|
81
|
+
provided as the command-line options, or you can use the standard AWS
|
82
|
+
environment variables:
|
83
|
+
|
84
|
+
```bash
|
85
|
+
export AWS_ACCESS_KEY_ID='...'
|
86
|
+
export AWS_SECRET_ACCESS_KEY='...'
|
87
|
+
export AWS_REGION='us-west-2'
|
88
|
+
```
|
89
|
+
|
90
|
+
## The SecurityGroups file
|
91
|
+
|
92
|
+
Specify your security groups in a `SecurityGroups` file in your project's root.
|
93
|
+
Declare security groups that you need and the ingresses you want to expose. You
|
94
|
+
can add ingresses using `tcp`, `udp`, and `ping`. For each ingress you can list
|
95
|
+
allowed hosts using group names or CIDR notation.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
group 'web' do
|
99
|
+
desc 'Web servers'
|
100
|
+
|
101
|
+
tcp 80
|
102
|
+
end
|
103
|
+
|
104
|
+
group 'db' do
|
105
|
+
desc 'database servers'
|
106
|
+
|
107
|
+
tcp 5432, 'web'
|
108
|
+
end
|
109
|
+
|
110
|
+
group 'log' do
|
111
|
+
desc 'log server'
|
112
|
+
|
113
|
+
tcp 9999, 'web', 'db', '10.1.0.0/16'
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
An environment can be specified which is available through the `env` variable.
|
118
|
+
This allows you to have custom security groups per server environment.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
group "web-#{env}"
|
122
|
+
group "db-#{env}" do
|
123
|
+
tcp 5432, "web-#{env}"
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
Your application may depend on security groups defined by other services. Ensure
|
128
|
+
they exist using the `depends` method.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
depends 'my-other-service'
|
132
|
+
group 'my-service' do
|
133
|
+
udp 9999, 'my-other-service'
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
You may specify port ranges for `tcp` and `udp` using the range operator.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
group 'my-service' do
|
141
|
+
udp 5000..9999, '0.0.0.0/0'
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
## Usage
|
146
|
+
|
147
|
+
Simply navigate to the directory containing your `SecurityGroups` file and run `holepunch`.
|
148
|
+
|
149
|
+
```
|
150
|
+
$ holepunch
|
151
|
+
```
|
152
|
+
|
153
|
+
If you need to specify an environment:
|
154
|
+
|
155
|
+
```
|
156
|
+
$ holepunch -e live
|
157
|
+
```
|
158
|
+
|
159
|
+
## Testing
|
160
|
+
|
161
|
+
You can run the unit tests by simply running rspec.
|
162
|
+
|
163
|
+
```
|
164
|
+
$ rspec
|
165
|
+
```
|
166
|
+
|
167
|
+
By default the integration tests with EC2 are not run. You may run them with:
|
168
|
+
|
169
|
+
```
|
170
|
+
$ rspec -t integration
|
171
|
+
```
|
172
|
+
|
173
|
+
## Authors
|
174
|
+
|
175
|
+
- Ben Scott (gamepoet@gmail.com)
|
176
|
+
- Pat Wyatt (pat@codeofhonor.com)
|
177
|
+
|
178
|
+
## License
|
179
|
+
|
180
|
+
Copyright 2014 Undead Labs, LLC.
|
181
|
+
|
182
|
+
Licensed under the MIT License: http://opensource.org/licenses/MIT
|
183
|
+
email:
|
184
|
+
- gamepoet@gmail.com
|
185
|
+
- pat@codeofhonor.com
|
186
|
+
executables:
|
187
|
+
- holepunch
|
188
|
+
extensions: []
|
189
|
+
extra_rdoc_files: []
|
190
|
+
files:
|
191
|
+
- bin/holepunch
|
192
|
+
- lib/holepunch.rb
|
193
|
+
- lib/holepunch/cli.rb
|
194
|
+
- lib/holepunch/definition.rb
|
195
|
+
- lib/holepunch/dsl.rb
|
196
|
+
- lib/holepunch/ec2.rb
|
197
|
+
- lib/holepunch/logger.rb
|
198
|
+
- lib/holepunch/version.rb
|
199
|
+
homepage: https://github.com/undeadlabs/holepunch
|
200
|
+
licenses:
|
201
|
+
- MIT
|
202
|
+
metadata: {}
|
203
|
+
post_install_message:
|
204
|
+
rdoc_options: []
|
205
|
+
require_paths:
|
206
|
+
- lib
|
207
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
208
|
+
requirements:
|
209
|
+
- - ">="
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: 1.9.3
|
212
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
213
|
+
requirements:
|
214
|
+
- - ">="
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
version: '0'
|
217
|
+
requirements: []
|
218
|
+
rubyforge_project:
|
219
|
+
rubygems_version: 2.2.2
|
220
|
+
signing_key:
|
221
|
+
specification_version: 4
|
222
|
+
summary: Manage AWS security groups in a declarative way
|
223
|
+
test_files: []
|