reyes 0.0.1 → 0.0.2
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 +8 -8
- data/.gitignore +2 -0
- data/.rubocop-disables.yml +11 -0
- data/.rubocop.yml +17 -0
- data/README.md +11 -1
- data/bin/reyes +95 -0
- data/config.yaml.example +13 -0
- data/lib/reyes.rb +15 -0
- data/lib/reyes/aws_manager.rb +116 -0
- data/lib/reyes/config.rb +21 -0
- data/lib/reyes/errors.rb +3 -0
- data/lib/reyes/group_manager.rb +414 -0
- data/lib/reyes/group_tools.rb +145 -0
- data/lib/reyes/ipset.rb +118 -0
- data/lib/reyes/iptables.rb +87 -0
- data/lib/reyes/run_generation.rb +75 -0
- data/lib/reyes/version.rb +1 -1
- data/reyes.gemspec +3 -0
- metadata +59 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
N2YwMTQ0NDA3OWVjMjQ3MTRkZWI4MTA3YTBlNWU3MTlhMDU5NzA2NA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MTM4NmQwZmVhY2EwOWJkZGQwODYzM2FkMDJiZGJkNDYyOTQ4NDFkZg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
NzNlN2IwMmMxNmUyNjkxMGYzYjljMTgyMTUwMDU5MjIwOTlmYTdiZmVhODBi
|
10
|
+
OWVhYzJiM2FiNjMzM2U5N2Q0NzIwYmEyNzAwYjQyOWNjMGQ2ZWYwZDE0MDFm
|
11
|
+
NzgyZTcwNzlkMzY3NDhjMTYxMWY1YTViMTU1MzVhZjc2NzBmZTc=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YjY5MTg0Y2RlYjBhMzlkZjA5Yjc1ZDkyZDgwMzdmMDVjYWNlNjEzNmE2MDVj
|
14
|
+
MDMwMzBiZDg4Nzc5NWM1ZjUzNDkzNzQ3YzQwMWY1NGMxMmU4MzJjMTcyYWY5
|
15
|
+
ODY5YzIwYmVkYWNmYWI4NjZiZWZjNDM1ZmZkZjFiOGExYjNhZmY=
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
inherit_from:
|
2
|
+
- .rubocop-disables.yml
|
3
|
+
|
4
|
+
Style/SpaceAroundEqualsInParameterDefault:
|
5
|
+
EnforcedStyle: no_space
|
6
|
+
|
7
|
+
Style/TrailingComma:
|
8
|
+
EnforcedStyleForMultiline: comma
|
9
|
+
|
10
|
+
Style/SignalException:
|
11
|
+
EnforcedStyle: only_raise
|
12
|
+
|
13
|
+
AllCops:
|
14
|
+
Include:
|
15
|
+
- '**/Rakefile'
|
16
|
+
Exclude:
|
17
|
+
- Gemfile
|
data/README.md
CHANGED
@@ -1,7 +1,17 @@
|
|
1
1
|
Reyes
|
2
2
|
=====
|
3
3
|
|
4
|
-

|
4
|
+

|
5
5
|
|
6
6
|
Reyes populates IPTables firewall rules based on EC2 security group rules.
|
7
|
+
Named after the Pt. Reyes Lighthouse, which shines light through the fog,
|
8
|
+
preventing your ships from crashing on the rocks as they make their way to
|
9
|
+
port.
|
7
10
|
|
11
|
+
Use Case
|
12
|
+
--------
|
13
|
+
|
14
|
+
Reyes is designed to apply security group rules to IPsec VPN traffic that would
|
15
|
+
otherwise be injected past security group protection. This is useful for
|
16
|
+
enforcing firewalls on VPNs between EC2 instances and security groups in other
|
17
|
+
VPCs, even in other regions.
|
data/bin/reyes
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require_relative '../lib/reyes'
|
4
|
+
|
5
|
+
def command_dump(options)
|
6
|
+
instance_id = options.fetch(:instance_id)
|
7
|
+
region = options.fetch(:region)
|
8
|
+
g = Reyes::GroupManager.new(region, instance_id, options[:config])
|
9
|
+
|
10
|
+
AWS.memoize do
|
11
|
+
puts g.do_stuff.to_yaml
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def command_install(options)
|
16
|
+
instance_id = options.fetch(:instance_id)
|
17
|
+
region = options.fetch(:region)
|
18
|
+
AWS.memoize do
|
19
|
+
g = Reyes::GroupManager.new(region, instance_id, options[:config])
|
20
|
+
|
21
|
+
options[:run_options][:interactive] = true # TODO
|
22
|
+
options[:run_options][:log_accept] = true # TODO
|
23
|
+
|
24
|
+
g.run!(options.fetch(:run_options))
|
25
|
+
|
26
|
+
if options[:prune]
|
27
|
+
g.prune_ipsets
|
28
|
+
# g.prune_iptables_rules
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def parse_args
|
36
|
+
options = {
|
37
|
+
:region => 'us-west-1',
|
38
|
+
:run_options => {},
|
39
|
+
}
|
40
|
+
|
41
|
+
optparse = OptionParser.new do |opts|
|
42
|
+
opts.banner = <<-EOM
|
43
|
+
usage: #{File.basename($0)} [options]
|
44
|
+
|
45
|
+
Manipulate IPTables rules based on EC2 security groups.
|
46
|
+
|
47
|
+
For example:
|
48
|
+
|
49
|
+
#{File.basename($0)} --prune --install $(facter -p ec2_instance_id)
|
50
|
+
|
51
|
+
Options:
|
52
|
+
EOM
|
53
|
+
|
54
|
+
opts.on('-c', '--config CONFIG', 'YAML config file') do |config|
|
55
|
+
options[:config] = config
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on('--region REGION', 'Set EC2 region') do |region|
|
59
|
+
options[:region] = region
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on('--prune', 'Prune old generations after creating rules') do
|
63
|
+
options[:prune] = true
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on('--install INSTANCE_ID', 'Configure this instance as INSTANCE_ID') do |id|
|
67
|
+
options[:command] = :install
|
68
|
+
options[:instance_id] = id
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on('--empty', 'Generate empty (default DROP) rules') do
|
72
|
+
options[:run_options][:empty] = true
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on('-h', '--help', 'Display this help message') do
|
76
|
+
STDERR.puts opts
|
77
|
+
exit 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
optparse.parse!
|
82
|
+
|
83
|
+
case options[:command]
|
84
|
+
when :dump
|
85
|
+
command_dump(options)
|
86
|
+
when :install
|
87
|
+
command_install(options)
|
88
|
+
else
|
89
|
+
STDERR.puts optparse
|
90
|
+
STDERR.puts "\nError: Must provide a command"
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
parse_args
|
data/config.yaml.example
ADDED
data/lib/reyes.rb
CHANGED
@@ -1,4 +1,19 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'chalk-log'
|
5
|
+
require 'subprocess'
|
6
|
+
|
1
7
|
module Reyes
|
2
8
|
end
|
3
9
|
|
4
10
|
require_relative './reyes/version'
|
11
|
+
require_relative './reyes/errors'
|
12
|
+
|
13
|
+
require_relative './reyes/aws_manager'
|
14
|
+
require_relative './reyes/config'
|
15
|
+
require_relative './reyes/group_manager'
|
16
|
+
require_relative './reyes/group_tools'
|
17
|
+
require_relative './reyes/ipset'
|
18
|
+
require_relative './reyes/iptables'
|
19
|
+
require_relative './reyes/run_generation'
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Reyes
|
4
|
+
class AwsManager
|
5
|
+
class Error < Reyes::Error; end
|
6
|
+
|
7
|
+
include Chalk::Log
|
8
|
+
|
9
|
+
def self.with_retries(retries=5, delay=2)
|
10
|
+
raise ArgumentError.new('Block is required') unless block_given?
|
11
|
+
begin
|
12
|
+
yield
|
13
|
+
rescue AWS::EC2::Errors::RequestLimitExceeded => err
|
14
|
+
log.warn(err.inspect)
|
15
|
+
if retries > 0
|
16
|
+
retries -= 1
|
17
|
+
sleep delay
|
18
|
+
retry
|
19
|
+
else
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(config_path=nil)
|
26
|
+
@config ||= Reyes::Config.new(config_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ec2(region)
|
30
|
+
connections.fetch(:ec2).fetch(region)
|
31
|
+
end
|
32
|
+
|
33
|
+
def connections
|
34
|
+
@connections ||= connect!
|
35
|
+
end
|
36
|
+
|
37
|
+
def regions
|
38
|
+
aws_config.fetch('regions')
|
39
|
+
end
|
40
|
+
|
41
|
+
def vpcs
|
42
|
+
return @vpcs if @vpcs
|
43
|
+
|
44
|
+
@vpcs = aws_config.fetch('vpcs').map do |row|
|
45
|
+
unless row.length == 2
|
46
|
+
raise Error.new("Invalid VPC row: #{row.inspect}")
|
47
|
+
end
|
48
|
+
|
49
|
+
region, vpc_id = row
|
50
|
+
vpc = ec2(region).vpcs[vpc_id]
|
51
|
+
|
52
|
+
# warm cidr_block cache & ensure VPC exists
|
53
|
+
vpc.cidr_block
|
54
|
+
|
55
|
+
vpc
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def security_groups(region)
|
60
|
+
ec2(region).security_groups.to_a
|
61
|
+
end
|
62
|
+
|
63
|
+
# Look up foreign VPC security groups by name.
|
64
|
+
#
|
65
|
+
# Does not use native AWS API filtering to promote better cache behavior.
|
66
|
+
#
|
67
|
+
# @param name [String]
|
68
|
+
# @return [Array<AWS::EC2::SecurityGroup>]
|
69
|
+
#
|
70
|
+
def vpc_security_groups_by_name(name)
|
71
|
+
unless name.is_a?(String)
|
72
|
+
raise ArgumentError.new("#{name.inspect} must be a String")
|
73
|
+
end
|
74
|
+
|
75
|
+
vpcs.map { |vpc|
|
76
|
+
vpc.security_groups.find_all {|sg| sg.name == name }
|
77
|
+
}.flatten
|
78
|
+
end
|
79
|
+
|
80
|
+
def warm_sg_cache
|
81
|
+
connections.fetch(:ec2).each_pair do |region, ec2|
|
82
|
+
log.debug("Warming security group cache for #{region}")
|
83
|
+
ec2.security_groups.to_a
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def aws_config
|
90
|
+
@config.aws_config
|
91
|
+
end
|
92
|
+
|
93
|
+
def connect!
|
94
|
+
conns = {
|
95
|
+
ec2: {},
|
96
|
+
}
|
97
|
+
|
98
|
+
regions.each do |region|
|
99
|
+
conns[:ec2][region] = connect_class(AWS::EC2, region)
|
100
|
+
end
|
101
|
+
|
102
|
+
conns
|
103
|
+
end
|
104
|
+
|
105
|
+
def connect_class(klass, region)
|
106
|
+
opts = {
|
107
|
+
region: region,
|
108
|
+
access_key_id: @config.aws_credentials.fetch(:access_key_id),
|
109
|
+
secret_access_key: @config.aws_credentials.fetch(:secret_access_key),
|
110
|
+
logger: Chalk::Log::Logger.new("#{klass.name}<#{region}>"),
|
111
|
+
}
|
112
|
+
|
113
|
+
klass.new(opts)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/reyes/config.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Reyes
|
4
|
+
class Config
|
5
|
+
def initialize(path=nil)
|
6
|
+
@path = path || File.expand_path('../../../config.yaml', __FILE__)
|
7
|
+
end
|
8
|
+
|
9
|
+
def aws_config
|
10
|
+
config.fetch('aws')
|
11
|
+
end
|
12
|
+
|
13
|
+
def aws_credentials
|
14
|
+
aws_config.fetch('credentials')
|
15
|
+
end
|
16
|
+
|
17
|
+
def config
|
18
|
+
@config ||= YAML.load_file(@path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/reyes/errors.rb
ADDED
@@ -0,0 +1,414 @@
|
|
1
|
+
module Reyes
|
2
|
+
|
3
|
+
# TODO: use a more precise name
|
4
|
+
class GroupManager
|
5
|
+
include Chalk::Log
|
6
|
+
|
7
|
+
# Short names for AWS regions to save space in ipset names
|
8
|
+
RegionShortNames = {
|
9
|
+
'us-east-1' => 'VA',
|
10
|
+
'us-west-2' => 'OR',
|
11
|
+
'us-west-1' => 'CA',
|
12
|
+
'eu-west-1' => 'IE',
|
13
|
+
'eu-central-1' => 'DE',
|
14
|
+
'ap-southeast-1' => 'SG',
|
15
|
+
'ap-southeast-2' => 'AU',
|
16
|
+
'ap-northeast-1' => 'JP',
|
17
|
+
'sa-east-1' => 'BR',
|
18
|
+
'us-gov-west-1' => 'GV',
|
19
|
+
'cn-north-1' => 'CN',
|
20
|
+
}
|
21
|
+
|
22
|
+
ReyesInputChain = 'reyes-ipsec-input'
|
23
|
+
|
24
|
+
attr_reader :aws
|
25
|
+
|
26
|
+
def initialize(region, instance_id, config_file=nil)
|
27
|
+
log.info("Initializing #{self.class.name} for #{region} #{instance_id}")
|
28
|
+
|
29
|
+
@aws = Reyes::AwsManager.new(config_file)
|
30
|
+
@instance_id = instance_id
|
31
|
+
@instance = @aws.ec2(region).instances[instance_id]
|
32
|
+
end
|
33
|
+
|
34
|
+
def our_groups
|
35
|
+
@our_groups ||= Reyes::AwsManager.with_retries {
|
36
|
+
@instance.security_groups
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Hash] options
|
41
|
+
#
|
42
|
+
# @option options :empty [Boolean] (false) Generate an empty (default DROP)
|
43
|
+
# rule sets without actually looking up security groups
|
44
|
+
# @option options :interactive [Boolean] (false) Whether to prompt for
|
45
|
+
# confirmation before applying rules
|
46
|
+
# @option options :log_accept [Boolean] (false) Whether to log packets on
|
47
|
+
# ACCEPT
|
48
|
+
#
|
49
|
+
def run!(options={})
|
50
|
+
options = {
|
51
|
+
empty: false,
|
52
|
+
interactive: false,
|
53
|
+
log_accept: false,
|
54
|
+
}.merge(options)
|
55
|
+
|
56
|
+
log_accept = options.fetch(:log_accept)
|
57
|
+
|
58
|
+
log.info("Starting iptables rule generation run!")
|
59
|
+
|
60
|
+
if options.fetch(:empty)
|
61
|
+
log.warn("Generating empty (default DROP) rule set")
|
62
|
+
data = generate_rules_empty
|
63
|
+
else
|
64
|
+
data = generate_rules
|
65
|
+
end
|
66
|
+
|
67
|
+
new_rules = generate_iptables_script_file(data, log_accept: log_accept)
|
68
|
+
new_ipsets = generate_ipsets(data)
|
69
|
+
|
70
|
+
show_iptables_diff(new_rules)
|
71
|
+
show_ipsets_diff(new_ipsets)
|
72
|
+
|
73
|
+
if options.fetch(:interactive)
|
74
|
+
puts 'Press enter to continue...'
|
75
|
+
STDIN.gets
|
76
|
+
end
|
77
|
+
|
78
|
+
materialize_ipsets(new_ipsets)
|
79
|
+
iptables_restore(new_rules)
|
80
|
+
|
81
|
+
# XXX(richo) Should we be pruning inside run! ?
|
82
|
+
log.info('Finished firewall configuration run')
|
83
|
+
end
|
84
|
+
|
85
|
+
# TODO: actually do some kind of diff or logging here?
|
86
|
+
def show_iptables_diff(new_rules)
|
87
|
+
log.info("Old rules:")
|
88
|
+
Subprocess.check_call(%w{iptables-save})
|
89
|
+
|
90
|
+
log.info("New rules:")
|
91
|
+
puts new_rules
|
92
|
+
end
|
93
|
+
|
94
|
+
def iptables_restore(new_rules)
|
95
|
+
log.info("Restoring #{new_rules.count("\n")} lines of iptables rules")
|
96
|
+
|
97
|
+
log.info('+ iptables-restore')
|
98
|
+
Subprocess.check_call(['iptables-restore'],
|
99
|
+
stdin: Subprocess::PIPE) do |p|
|
100
|
+
p.communicate(new_rules)
|
101
|
+
end
|
102
|
+
log.info('restored')
|
103
|
+
end
|
104
|
+
|
105
|
+
def generate_rules_empty
|
106
|
+
{:groups => {}, :ipsets => {}}
|
107
|
+
end
|
108
|
+
|
109
|
+
# Given our instance ID and security group rules, generate IPTables rules
|
110
|
+
# needed to emulate security group behavior for foreign VPCs.
|
111
|
+
#
|
112
|
+
# Look up the set of instance IP addresses in any remote VPC security
|
113
|
+
# groups referenced by our rules, and add them to a list of ipsets to
|
114
|
+
# create.
|
115
|
+
#
|
116
|
+
# Also create a list of IPTables rules that will reference these ipsets.
|
117
|
+
#
|
118
|
+
# This method generates the data and returns it as a hash without making
|
119
|
+
# any changes to the system beyond incrementing the run generation (used
|
120
|
+
# for ipset garbage collection).
|
121
|
+
#
|
122
|
+
def generate_rules
|
123
|
+
run_generation_increment!
|
124
|
+
log.info("Generating rules for generation #{run_generation}")
|
125
|
+
|
126
|
+
data = generate_rules_empty
|
127
|
+
|
128
|
+
needed_groups = {}
|
129
|
+
|
130
|
+
our_groups.each do |group|
|
131
|
+
group_key = "#{group.group_id}:#{group.name}"
|
132
|
+
data[:groups][group_key] = []
|
133
|
+
|
134
|
+
# we only work with ingress permissions
|
135
|
+
group.ingress_ip_permissions.each do |perm|
|
136
|
+
perm_hash = {
|
137
|
+
protocol: perm.protocol,
|
138
|
+
port: perm.port_range,
|
139
|
+
remote_addrs: [],
|
140
|
+
remote_sets: [],
|
141
|
+
}
|
142
|
+
|
143
|
+
case perm.protocol
|
144
|
+
when :icmp
|
145
|
+
log.info("Skipping ICMP rule")
|
146
|
+
next
|
147
|
+
when :tcp, :udp
|
148
|
+
|
149
|
+
# append IP ranges
|
150
|
+
perm_hash[:remote_addrs] += perm.ip_ranges
|
151
|
+
|
152
|
+
# list the security groups we'll need to look up
|
153
|
+
perm.groups.each do |g|
|
154
|
+
foreign = foreign_groups_by_name(g.name)
|
155
|
+
foreign.each do |fg|
|
156
|
+
needed_groups[fg.group_id] = fg
|
157
|
+
perm_hash[:remote_sets] << ipset_name_for_group(fg)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
else
|
162
|
+
raise Error.new("Unexpected protocol #{perm.protocol.inspect}")
|
163
|
+
end
|
164
|
+
|
165
|
+
data[:groups][group_key] << perm_hash
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
data[:ipsets] = {}
|
170
|
+
needed_groups.each_value do |group|
|
171
|
+
data[:ipsets][ipset_name_for_group(group)] = \
|
172
|
+
addresses_for_group(group)
|
173
|
+
end
|
174
|
+
|
175
|
+
data
|
176
|
+
end
|
177
|
+
|
178
|
+
def generate_ipsets(data)
|
179
|
+
data.fetch(:ipsets).map do |name, hosts|
|
180
|
+
log.info "Creating IPSet for #{name.inspect}"
|
181
|
+
builder = IPSetBuilder.new(name)
|
182
|
+
hosts.each do |host|
|
183
|
+
log.info " - #{host.inspect}"
|
184
|
+
builder << host
|
185
|
+
end
|
186
|
+
|
187
|
+
builder
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def show_ipsets_diff(new_ipsets)
|
192
|
+
# TODO(richo)
|
193
|
+
end
|
194
|
+
|
195
|
+
def materialize_ipsets(new_ipsets)
|
196
|
+
new_ipsets.each do |ipset|
|
197
|
+
ipset.build
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# TODO: delurk
|
202
|
+
def create_iptables_rules(data)
|
203
|
+
data.fetch(:groups).each do |cluster, items|
|
204
|
+
log.info "Creating rules for #{cluster}"
|
205
|
+
items.each do |item|
|
206
|
+
rules = Reyes::IPTables.generate_rules_from_hash(item)
|
207
|
+
rules.each do |rule|
|
208
|
+
log.info(" #{rule.cmd.join(" ")}")
|
209
|
+
rule.materialize
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def prune_ipsets
|
216
|
+
log.info('Pruning old IPSets')
|
217
|
+
old_ipsets = []
|
218
|
+
current_ipsets = []
|
219
|
+
|
220
|
+
current_gen = run_generation
|
221
|
+
Reyes::IPSet.load_all.each do |set|
|
222
|
+
s = Reyes::GroupManager.parse_ipset_name(set.name)
|
223
|
+
unless s
|
224
|
+
log.warn("Skipping unparseable ipset name #{set.name.inspect}")
|
225
|
+
next
|
226
|
+
end
|
227
|
+
|
228
|
+
if s[:generation] < current_gen
|
229
|
+
old_ipsets << set
|
230
|
+
elsif s[:generation] == current_gen
|
231
|
+
current_ipsets << set
|
232
|
+
else
|
233
|
+
log.error("IPSet from a future generation detected: #{set.inspect}")
|
234
|
+
log.error("Cowardly refusing to proceed")
|
235
|
+
raise Reyes::Error.new("IPSet from future generation detected")
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
if current_ipsets.empty?
|
240
|
+
log.error("No IPSets from current generation.")
|
241
|
+
log.error("Cowardly refusing to proceed")
|
242
|
+
raise Reyes::Error.new("Pruning would remove all IPSets")
|
243
|
+
end
|
244
|
+
|
245
|
+
old_ipsets.each do |set|
|
246
|
+
log.info("Pruning IPSet: #{set.name}")
|
247
|
+
set.drop!
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# @return [Integer]
|
252
|
+
def run_generation
|
253
|
+
@generation ||= Reyes::RunGeneration.new
|
254
|
+
@generation.value
|
255
|
+
end
|
256
|
+
|
257
|
+
# Increment the run generation and persist it to disk
|
258
|
+
def run_generation_increment!
|
259
|
+
run_generation
|
260
|
+
@generation.increment!
|
261
|
+
end
|
262
|
+
|
263
|
+
# @return [String] A string title, at most 31 characters long
|
264
|
+
def ipset_name_for_group(group)
|
265
|
+
[
|
266
|
+
run_generation.to_s,
|
267
|
+
RegionShortNames.fetch(group.client.instance_variable_get(:@region)),
|
268
|
+
group.group_id,
|
269
|
+
group.name,
|
270
|
+
].join(':')[0...31]
|
271
|
+
end
|
272
|
+
|
273
|
+
# @return [Hash] the parsed names, or nil in case of error
|
274
|
+
def self.parse_ipset_name(name)
|
275
|
+
parts = name.split(":")
|
276
|
+
return nil unless parts.length == 4
|
277
|
+
|
278
|
+
generation, region, group_id, group_name = parts
|
279
|
+
return {
|
280
|
+
generation: Integer(generation),
|
281
|
+
region: region,
|
282
|
+
group_id: group_id,
|
283
|
+
group_name: group_name
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
# Look up addresses for given group in remote VPCs.
|
288
|
+
#
|
289
|
+
# @param group [AWS::EC2::SecurityGroup]
|
290
|
+
#
|
291
|
+
# @return [Array<String>] A list of private instance IP addresses
|
292
|
+
#
|
293
|
+
def addresses_for_group(group)
|
294
|
+
groups = foreign_groups_by_name(group.name)
|
295
|
+
groups.map { |g| g.instances.map(&:private_ip_address) }.flatten
|
296
|
+
end
|
297
|
+
|
298
|
+
# Look up remote VPC security groups by name.
|
299
|
+
#
|
300
|
+
# @param name [String]
|
301
|
+
#
|
302
|
+
# @return [Array<AWS::EC2::SecurityGroup>]
|
303
|
+
#
|
304
|
+
def foreign_groups_by_name(name)
|
305
|
+
aws.vpc_security_groups_by_name(name)
|
306
|
+
end
|
307
|
+
|
308
|
+
# @param [Hash] data
|
309
|
+
#
|
310
|
+
# @param [Hash] options
|
311
|
+
#
|
312
|
+
# @option options [Boolean] log_drop (true)
|
313
|
+
# @option options [Boolean] log_accept (false)
|
314
|
+
#
|
315
|
+
def generate_iptables_script_file(data, options={})
|
316
|
+
log.info("Generating script for iptables-restore")
|
317
|
+
|
318
|
+
options = {
|
319
|
+
log_drop: true,
|
320
|
+
log_accept: false,
|
321
|
+
}.merge(options)
|
322
|
+
|
323
|
+
log_drop = options.fetch(:log_drop)
|
324
|
+
log_accept = options.fetch(:log_accept)
|
325
|
+
|
326
|
+
lines = []
|
327
|
+
|
328
|
+
lines << "# Generated by Reyes v#{Reyes::VERSION} at #{Time.now.to_s}"
|
329
|
+
|
330
|
+
# we use the default filter table
|
331
|
+
lines << '*filter'
|
332
|
+
|
333
|
+
lines << ':INPUT ACCEPT'
|
334
|
+
lines << ':FORWARD DROP'
|
335
|
+
lines << ':OUTPUT ACCEPT'
|
336
|
+
lines << ''
|
337
|
+
|
338
|
+
lines << ":#{ReyesInputChain} -"
|
339
|
+
lines << ':reyes-log-drop -'
|
340
|
+
lines << ':reyes-log-accept -'
|
341
|
+
lines << ':reyes-accept -'
|
342
|
+
lines << ':reyes-drop -'
|
343
|
+
lines << ''
|
344
|
+
|
345
|
+
lines << IPTables.log_rule_string('reyes-log-drop', 'REYES BLOCK')
|
346
|
+
lines << IPTables.log_rule_string('reyes-log-accept', 'REYES ACCEPT')
|
347
|
+
|
348
|
+
lines << '-A reyes-drop -j reyes-log-drop' if log_drop
|
349
|
+
lines << '-A reyes-drop -j DROP'
|
350
|
+
lines << '-A reyes-accept -j reyes-log-accept' if log_accept
|
351
|
+
lines << '-A reyes-accept -j ACCEPT'
|
352
|
+
|
353
|
+
# filter all ipsec tunneled traffic through reyes
|
354
|
+
lines << "-A INPUT -m policy --pol ipsec --dir in -j #{ReyesInputChain}"
|
355
|
+
|
356
|
+
# allow normal ICMP traffic
|
357
|
+
IPTables.innocuous_icmp_rules(ReyesInputChain).each do |r|
|
358
|
+
lines << r.join(' ')
|
359
|
+
end
|
360
|
+
|
361
|
+
# allow established connections without logging
|
362
|
+
lines << "-A #{ReyesInputChain} -m conntrack --ctstate " \
|
363
|
+
"RELATED,ESTABLISHED -j ACCEPT"
|
364
|
+
|
365
|
+
# drop invalid connections
|
366
|
+
lines << "-A #{ReyesInputChain} -m conntrack --ctstate " \
|
367
|
+
"INVALID -j reyes-drop"
|
368
|
+
|
369
|
+
# add dynamic rules
|
370
|
+
lines << ''
|
371
|
+
lines << '# dynamic rules from security groups'
|
372
|
+
dynamic_rules_from_data(data).each do |rule|
|
373
|
+
lines << rule
|
374
|
+
end
|
375
|
+
|
376
|
+
# add reyes-drop to very end of reyes-ipsec-input chain
|
377
|
+
lines << ''
|
378
|
+
lines << '# default drop'
|
379
|
+
lines << "-A #{ReyesInputChain} -j reyes-drop"
|
380
|
+
|
381
|
+
# commit the filter table
|
382
|
+
lines << ''
|
383
|
+
lines << 'COMMIT'
|
384
|
+
|
385
|
+
text = lines.join("\n")
|
386
|
+
text << "\n"
|
387
|
+
|
388
|
+
text
|
389
|
+
end
|
390
|
+
|
391
|
+
private
|
392
|
+
|
393
|
+
def dynamic_rules_from_data(data)
|
394
|
+
log.info("Generating dynamic iptables rules")
|
395
|
+
|
396
|
+
rule_list = []
|
397
|
+
|
398
|
+
data.fetch(:groups).each do |cluster, items|
|
399
|
+
log.info "Generating rules for #{cluster}"
|
400
|
+
items.each do |item|
|
401
|
+
rules = Reyes::IPTables.generate_rules_from_hash(item,
|
402
|
+
ReyesInputChain,
|
403
|
+
'reyes-accept')
|
404
|
+
rules.each do |rule|
|
405
|
+
log.debug(' ' + rule.inspect)
|
406
|
+
rule_list << rule.join(' ')
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
rule_list
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Reyes
|
2
|
+
# A few methods for manipulating EC2 SecurityGroup and IpPermission objects
|
3
|
+
# that really should be in the aws-sdk. These are taken directly from
|
4
|
+
# space-commander.
|
5
|
+
module GroupTools
|
6
|
+
|
7
|
+
# Print a pretty representation of an AWS::EC2::SecurityGroup::IpPermission
|
8
|
+
# object. May issue API calls to resolve security group names.
|
9
|
+
#
|
10
|
+
# @param [AWS::EC2::SecurityGroup::IpPermission] perm Object to inspect
|
11
|
+
#
|
12
|
+
# @param [Hash] options
|
13
|
+
#
|
14
|
+
# @option options [Integer] :indent Indentation level (default 0)
|
15
|
+
#
|
16
|
+
# @option options [Boolean] :include_group Whether to print the security
|
17
|
+
# group to which the IpPermission belongs. (default true)
|
18
|
+
#
|
19
|
+
# @option options [SpaceCommander::SecurityGroup::Cache] :cache A cache to
|
20
|
+
# use for resolving security group names as needed.
|
21
|
+
#
|
22
|
+
def self.pretty_perm(perm, options={})
|
23
|
+
options = {:indent => 0, :include_group => true}.merge(options)
|
24
|
+
indent = options.fetch(:indent)
|
25
|
+
include_group = options.fetch(:include_group)
|
26
|
+
|
27
|
+
# use cache to find groups if one was provided
|
28
|
+
if options[:cache]
|
29
|
+
groups = options[:cache].unsafe_get_many(perm.groups)
|
30
|
+
else
|
31
|
+
groups = perm.groups
|
32
|
+
end
|
33
|
+
|
34
|
+
lines = []
|
35
|
+
group = perm.security_group
|
36
|
+
lines << "security_group: #{group.name} (#{group.id})" if include_group
|
37
|
+
lines.concat([
|
38
|
+
"type: #{(perm.egress? ? :egress : :ingress)}",
|
39
|
+
"proto: #{perm.protocol.inspect}",
|
40
|
+
"ports: #{perm.port_range.inspect}",
|
41
|
+
"cidr: #{perm.ip_ranges.inspect}",
|
42
|
+
"groups: [#{groups.map{|g| "<#{g.name} #{g.id}>"}.join(", ")}]"
|
43
|
+
])
|
44
|
+
return lines.map{|line| ' '*indent + line}.join("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
# Inspect an AWS::EC2::SecurityGroup::IpPermission object. May issue API
|
48
|
+
# calls to resolve security group names.
|
49
|
+
#
|
50
|
+
# @param [AWS::EC2::SecurityGroup::IpPermission] perm Object to inspect
|
51
|
+
#
|
52
|
+
# @param [Hash] options
|
53
|
+
#
|
54
|
+
# @option options [SpaceCommander::SecurityGroup::Cache] :cache A cache to
|
55
|
+
# use for resolving security group names as needed.
|
56
|
+
#
|
57
|
+
def self.inspect_perm(perm, options={})
|
58
|
+
unless perm.is_a?(AWS::EC2::SecurityGroup::IpPermission)
|
59
|
+
raise ArgumentError.new("Not an IpPermission: #{perm.inspect}")
|
60
|
+
end
|
61
|
+
|
62
|
+
s = "#<IpPermission #{perm.egress? ? :egress : :ingress}"
|
63
|
+
|
64
|
+
s << " @security_group=<#{perm.security_group.id} #{perm.security_group.name}>"
|
65
|
+
s << " @protocol=#{perm.protocol.inspect}"
|
66
|
+
s << " @port_range=#{perm.port_range.inspect}"
|
67
|
+
|
68
|
+
unless perm.ip_ranges.empty?
|
69
|
+
s << " @ip_ranges=#{perm.ip_ranges.inspect}"
|
70
|
+
end
|
71
|
+
|
72
|
+
unless perm.groups.empty?
|
73
|
+
# use cache to find groups if one was provided
|
74
|
+
if options[:cache]
|
75
|
+
groups = options[:cache].unsafe_get_many(perm.groups)
|
76
|
+
else
|
77
|
+
groups = perm.groups
|
78
|
+
end
|
79
|
+
|
80
|
+
s << " @groups=[#{groups.map{|g| "<#{g.name} #{g.id}>"}.join(", ")}]"
|
81
|
+
end
|
82
|
+
s
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.perm_to_hash(perm)
|
86
|
+
h = {}
|
87
|
+
|
88
|
+
h[:protocol] = perm.protocol.to_s
|
89
|
+
h[:label] = ''
|
90
|
+
|
91
|
+
port = perm.port_range
|
92
|
+
if port == all_ports(perm.protocol)
|
93
|
+
h[:port] = 'all'
|
94
|
+
else
|
95
|
+
if port.first == port.last
|
96
|
+
h[:port] = port.first
|
97
|
+
else
|
98
|
+
h[:port] = "#{port.first}-#{port.last}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# TODO: convert /32s back into hostname from config.yaml if possible
|
103
|
+
if perm.ip_ranges.length > 1
|
104
|
+
h[:cidr] = perm.ip_ranges
|
105
|
+
elsif perm.ip_ranges.length == 1
|
106
|
+
h[:cidr] = perm.ip_ranges.first
|
107
|
+
if h[:cidr] == '0.0.0.0/0'
|
108
|
+
h[:cidr] = 'all'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
if perm.groups.length > 0
|
113
|
+
h[:groups] = perm.groups.map(&:name)
|
114
|
+
end
|
115
|
+
|
116
|
+
h
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.group_to_hash(group)
|
120
|
+
h = {
|
121
|
+
:name => group.name,
|
122
|
+
:description => group.description,
|
123
|
+
:inbound => group.ingress_ip_permissions.map {|p| perm_to_hash(p)},
|
124
|
+
:outbound => group.egress_ip_permissions.map {|p| perm_to_hash(p)},
|
125
|
+
}
|
126
|
+
|
127
|
+
h
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.all_ports(protocol)
|
131
|
+
case protocol
|
132
|
+
when :icmp
|
133
|
+
-1..-1
|
134
|
+
when :tcp, :udp
|
135
|
+
0..65535
|
136
|
+
when :any, :'-1', -1
|
137
|
+
nil
|
138
|
+
else
|
139
|
+
msg = "Don't know how to allow 'all' ports for " + protocol.inspect
|
140
|
+
raise NotImplementedError.new(msg)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
data/lib/reyes/ipset.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
module Reyes
|
2
|
+
class IPSetBuilder
|
3
|
+
TYPE = "hash:ip"
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
# Constructor for ipsets, maintaining the invariant that once constructed,
|
7
|
+
# ipsets will not be altered.
|
8
|
+
# @param [String] name of the IPSet to create
|
9
|
+
def initialize(name)
|
10
|
+
@name = name
|
11
|
+
@members = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add a new member to this set. `member` should be something an ipset can
|
15
|
+
# understand, eg an ip address or cidr range
|
16
|
+
# @param [String] entity to add to the set
|
17
|
+
def <<(entity)
|
18
|
+
@members << entity
|
19
|
+
end
|
20
|
+
|
21
|
+
# Builds the ipset, retuning an IPSet
|
22
|
+
# @returns [IPSet]
|
23
|
+
def build
|
24
|
+
create
|
25
|
+
populate
|
26
|
+
IPSet.load(@name)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def create
|
32
|
+
begin
|
33
|
+
Subprocess.check_call(["ipset", "create", @name, TYPE])
|
34
|
+
rescue Subprocess::NonZeroExit
|
35
|
+
raise Error.new("Couldn't create ipset #@name")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def populate
|
40
|
+
@members.each do |e|
|
41
|
+
begin
|
42
|
+
Subprocess.check_call(["ipset", "add", @name, e])
|
43
|
+
rescue Subprocess::NonZeroExit
|
44
|
+
raise Error.new("Couldn't add #{e} to ipset #@name")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Error < StandardError
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class IPSet < Struct.new(:name, :type, :header, :size, :references, :members)
|
54
|
+
# An immutable view into an existing IPSet
|
55
|
+
|
56
|
+
# @param [Name] name of the set to load
|
57
|
+
# @returns IPSet
|
58
|
+
def self.load(name)
|
59
|
+
new(*load_by_name(name))
|
60
|
+
end
|
61
|
+
|
62
|
+
# Loads all IPSets from the local system
|
63
|
+
# @returns [Array<IPSet>]
|
64
|
+
def self.load_all
|
65
|
+
cmd = ['ipset', 'list']
|
66
|
+
output = Subprocess.check_output(cmd)
|
67
|
+
return output.split("\n\n").map { |x| new(*parse(x)) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Drops the currect IPSet, destroying it on the underlying system
|
71
|
+
def drop!
|
72
|
+
begin
|
73
|
+
Subprocess.check_call(["ipset", "destroy", name])
|
74
|
+
rescue Subprocess::NonZeroExit
|
75
|
+
raise Error.new("Couldn't destroy #{name}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def self.load_by_name(name)
|
82
|
+
cmd = ['ipset', 'list', name]
|
83
|
+
output = Subprocess.check_output(cmd)
|
84
|
+
entries = output.split("\n\n")
|
85
|
+
unless entries.length == 1
|
86
|
+
raise IPSetError.new("`ipset list #{name}` returned too many entries")
|
87
|
+
end
|
88
|
+
|
89
|
+
return parse(entries.first)
|
90
|
+
end
|
91
|
+
|
92
|
+
KEYS = ["Name", "Header", "Size in memory", "References", "Members", "Type"]
|
93
|
+
def self.parse(entry)
|
94
|
+
out = {:members => []}
|
95
|
+
entry.split(?\n).each do |line|
|
96
|
+
k, v = line.split(":", 2)
|
97
|
+
if KEYS.include?(k)
|
98
|
+
out[k] = v.strip
|
99
|
+
else
|
100
|
+
raise Error.new("Invalid data: #{entry.inspect}") unless out.include?("Members")
|
101
|
+
out[:members] << line.strip
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
return [
|
106
|
+
out["Name"],
|
107
|
+
out["Type"],
|
108
|
+
out["Header"],
|
109
|
+
Integer(out["Size in memory"]),
|
110
|
+
Integer(out["References"]),
|
111
|
+
out[:members],
|
112
|
+
]
|
113
|
+
end
|
114
|
+
|
115
|
+
class Error < StandardError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Reyes
|
2
|
+
# A collection of functionality related to generating IPTables rule sets.
|
3
|
+
module IPTables
|
4
|
+
|
5
|
+
# Generate IPTables rule arguments based on a specification and yield a
|
6
|
+
# series of arguments appropriate for passing to IPTables.
|
7
|
+
#
|
8
|
+
# @param [Symbol] protocol
|
9
|
+
# @param [Range] dport_range
|
10
|
+
# @param [Array<String>] remote_addrs
|
11
|
+
# @param [Array<String>] remote_sets
|
12
|
+
# @param [String] input_chain
|
13
|
+
# @param [String] accept_chain
|
14
|
+
#
|
15
|
+
# @yield [Array<String>] IPTables rules argument array
|
16
|
+
#
|
17
|
+
def self.generate_rules(protocol, dport_range, remote_addrs, remote_sets,
|
18
|
+
input_chain, accept_chain)
|
19
|
+
raise 'Unsupported protocol' unless [:tcp, :udp].include?(protocol)
|
20
|
+
|
21
|
+
unless block_given?
|
22
|
+
return enum_for(__method__, protocol, dport_range, remote_addrs,
|
23
|
+
remote_sets, input_chain, accept_chain)
|
24
|
+
end
|
25
|
+
|
26
|
+
cmd = ['-A', input_chain, '-p', protocol.to_s]
|
27
|
+
|
28
|
+
if dport_range.first == dport_range.last
|
29
|
+
# single port match
|
30
|
+
cmd += ['--dport', dport_range.first.to_s]
|
31
|
+
else
|
32
|
+
# port range match
|
33
|
+
cmd += ['-m', 'multiport', '--dports',
|
34
|
+
"#{dport_range.first}:#{dport_range.last}"]
|
35
|
+
end
|
36
|
+
|
37
|
+
jump_args = ['-j', accept_chain]
|
38
|
+
|
39
|
+
remote_addrs.each do |addr|
|
40
|
+
yield cmd + ['-s', addr] + jump_args
|
41
|
+
end
|
42
|
+
|
43
|
+
remote_sets.each do |set|
|
44
|
+
yield cmd + ['-m', 'set', '--match-set', set, 'src,dst'] + jump_args
|
45
|
+
end
|
46
|
+
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# Generate IPTables rules from a hash. This is a thin wrapper around
|
51
|
+
# {.generate_rules}.
|
52
|
+
#
|
53
|
+
# @param [Hash] hash
|
54
|
+
# @param [String] input_chain
|
55
|
+
# @param [String] accept_chain
|
56
|
+
#
|
57
|
+
def self.generate_rules_from_hash(hash, input_chain, accept_chain)
|
58
|
+
generate_rules(hash.fetch(:protocol), hash.fetch(:port),
|
59
|
+
hash.fetch(:remote_addrs), hash.fetch(:remote_sets),
|
60
|
+
input_chain, accept_chain)
|
61
|
+
end
|
62
|
+
|
63
|
+
class Rule
|
64
|
+
IPTABLES = "iptables"
|
65
|
+
attr_reader :cmd
|
66
|
+
def initialize(cmd)
|
67
|
+
@cmd = cmd
|
68
|
+
end
|
69
|
+
|
70
|
+
def materialize
|
71
|
+
cmd = [IPTABLES] + @cmd
|
72
|
+
Subprocess.check_call(cmd)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.log_rule_string(chain, message, limit='3/min', limit_burst=10)
|
77
|
+
"-A #{chain} -m limit --limit #{limit} --limit-burst #{limit_burst}" \
|
78
|
+
" -j LOG --log-prefix \"[#{message}] \""
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.innocuous_icmp_rules(chain)
|
82
|
+
[3, 4, 11, 12, 8].map do |type|
|
83
|
+
%W{-A #{chain} -p icmp -m icmp --icmp-type #{type} -j ACCEPT}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Reyes
|
2
|
+
# An abstraction over tmpfile based generation number storage.
|
3
|
+
class RunGeneration
|
4
|
+
class LoadError < Reyes::Error; end
|
5
|
+
|
6
|
+
include Chalk::Log
|
7
|
+
|
8
|
+
DefaultGenerationPath = "/tmp/reyes.u#{Process.euid}.generation"
|
9
|
+
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
# @param path [String] Path to generation file
|
13
|
+
#
|
14
|
+
# If the file at `path` does not exist, it will be created. Attempting to
|
15
|
+
# open a symbolic link will raise Errno::ELOOP to avoid security
|
16
|
+
# vulnerabilities.
|
17
|
+
#
|
18
|
+
def initialize(path=DefaultGenerationPath)
|
19
|
+
@path = path
|
20
|
+
reload
|
21
|
+
end
|
22
|
+
|
23
|
+
# Increment the generation value and save it to disk.
|
24
|
+
#
|
25
|
+
# @return [Integer] new generation value
|
26
|
+
#
|
27
|
+
def increment!
|
28
|
+
@value += 1
|
29
|
+
log.debug("Incrementing generation to #{@value}")
|
30
|
+
save
|
31
|
+
@value
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def reload
|
37
|
+
log.debug("Opening generation file at #{@path.inspect}")
|
38
|
+
@fh = open(@path)
|
39
|
+
if @fh.size == 0
|
40
|
+
@value = 0
|
41
|
+
else
|
42
|
+
@value = load_value
|
43
|
+
end
|
44
|
+
log.debug("Loaded generation value #{@value.inspect}")
|
45
|
+
end
|
46
|
+
|
47
|
+
# Open `path` read-write, creating it if it doesn't exist, refusing to
|
48
|
+
# follow symbolic links in the final path component.
|
49
|
+
#
|
50
|
+
# @param [String] path
|
51
|
+
#
|
52
|
+
# @raise [SystemCallError] in various circumstances involving failure to
|
53
|
+
# open or create the file.
|
54
|
+
#
|
55
|
+
def open(path)
|
56
|
+
File.open(path, File::RDWR | File::CREAT | File::NOFOLLOW)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_value
|
60
|
+
@fh.rewind
|
61
|
+
content = @fh.read
|
62
|
+
if content.empty?
|
63
|
+
0
|
64
|
+
else
|
65
|
+
Integer(content)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def save
|
70
|
+
@fh.rewind
|
71
|
+
@fh.puts(@value.to_s)
|
72
|
+
@fh.flush
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/reyes/version.rb
CHANGED
data/reyes.gemspec
CHANGED
@@ -35,7 +35,10 @@ Gem::Specification.new do |gem|
|
|
35
35
|
gem.version = Reyes::VERSION
|
36
36
|
|
37
37
|
gem.add_dependency 'aws-sdk', '~> 1.60'
|
38
|
+
gem.add_dependency 'chalk-log'
|
39
|
+
gem.add_dependency 'subprocess'
|
38
40
|
|
39
41
|
gem.add_development_dependency 'pry'
|
40
42
|
gem.add_development_dependency 'rake'
|
43
|
+
gem.add_development_dependency 'rubocop'
|
41
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reyes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Brody
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-02-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -25,6 +25,34 @@ dependencies:
|
|
25
25
|
- - ~>
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '1.60'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: chalk-log
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: subprocess
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
28
56
|
- !ruby/object:Gem::Dependency
|
29
57
|
name: pry
|
30
58
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,18 +81,46 @@ dependencies:
|
|
53
81
|
- - ! '>='
|
54
82
|
- !ruby/object:Gem::Version
|
55
83
|
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rubocop
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
56
98
|
description: ! " Reyes is a gem...\n\n TO DO\n"
|
57
99
|
email:
|
58
100
|
- security@stripe.com
|
59
|
-
executables:
|
101
|
+
executables:
|
102
|
+
- reyes
|
60
103
|
extensions: []
|
61
104
|
extra_rdoc_files: []
|
62
105
|
files:
|
106
|
+
- .gitignore
|
107
|
+
- .rubocop-disables.yml
|
108
|
+
- .rubocop.yml
|
63
109
|
- .ruby-version
|
64
110
|
- Gemfile
|
65
111
|
- README.md
|
66
112
|
- Rakefile
|
113
|
+
- bin/reyes
|
114
|
+
- config.yaml.example
|
67
115
|
- lib/reyes.rb
|
116
|
+
- lib/reyes/aws_manager.rb
|
117
|
+
- lib/reyes/config.rb
|
118
|
+
- lib/reyes/errors.rb
|
119
|
+
- lib/reyes/group_manager.rb
|
120
|
+
- lib/reyes/group_tools.rb
|
121
|
+
- lib/reyes/ipset.rb
|
122
|
+
- lib/reyes/iptables.rb
|
123
|
+
- lib/reyes/run_generation.rb
|
68
124
|
- lib/reyes/version.rb
|
69
125
|
- reyes.gemspec
|
70
126
|
homepage: ''
|