demeter-cli 0.0.4
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 +7 -0
- checksums.yaml.gz.sig +2 -0
- data.tar.gz.sig +3 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +0 -0
- data/CONTRIBUTING.md +50 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.md +21 -0
- data/README.md +62 -0
- data/Rakefile +14 -0
- data/bin/demeter +11 -0
- data/certs/coinbase.pem +21 -0
- data/demeter.gemspec +35 -0
- data/lib/demeter.rb +48 -0
- data/lib/demeter/aws/manage_security_groups.rb +113 -0
- data/lib/demeter/aws/security_group.rb +371 -0
- data/lib/demeter/cli.rb +92 -0
- data/lib/demeter/commands/apply.rb +15 -0
- data/lib/demeter/commands/base.rb +21 -0
- data/lib/demeter/commands/generate.rb +109 -0
- data/lib/demeter/commands/plan.rb +40 -0
- data/lib/demeter/commands/status.rb +42 -0
- data/lib/demeter/version.rb +6 -0
- data/spec/ec2_stub.rb +77 -0
- data/spec/manage_security_groups_spec.rb +72 -0
- data/spec/projects/simple/ec2_apollo.yml +17 -0
- data/spec/projects/with_vars/ec2_apollo.yml +17 -0
- data/spec/projects/with_vars/ec2_bastion.yml +12 -0
- data/spec/security_group_spec.rb +73 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/variables/development.yml +2 -0
- data/spec/variables/global.yml +5 -0
- metadata +247 -0
- metadata.gz.sig +3 -0
@@ -0,0 +1,371 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'hashdiff'
|
3
|
+
|
4
|
+
module Demeter
|
5
|
+
module Aws
|
6
|
+
class SecurityGroup
|
7
|
+
def initialize(ec2)
|
8
|
+
@ec2 = ec2
|
9
|
+
@_sg = {}
|
10
|
+
@_local_sg = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def group_id
|
14
|
+
Demeter::vars["security_group.#{project_key}.id"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_aws(object)
|
18
|
+
if object.is_a?(::Aws::EC2::Types::SecurityGroup)
|
19
|
+
name_tag = object['tags'].detect{|tag| tag['key'].downcase == 'name'}
|
20
|
+
@_sg[:name] = (name_tag ? name_tag['value'] : object.group_name)
|
21
|
+
@_sg[:description] = object.description
|
22
|
+
@_sg[:vpc_id] = object.vpc_id
|
23
|
+
@_sg[:ingress] = []
|
24
|
+
@_sg[:egress] = []
|
25
|
+
|
26
|
+
Demeter::set_var("security_group.#{project_key}.id", object.group_id)
|
27
|
+
|
28
|
+
# INGRESS
|
29
|
+
object.ip_permissions.each do |rule|
|
30
|
+
rule.ip_ranges.each do |cidr_block|
|
31
|
+
@_sg[:ingress] << {
|
32
|
+
protocol: rule.ip_protocol.to_s,
|
33
|
+
from_port: rule.from_port.to_i,
|
34
|
+
to_port: rule.to_port.to_i,
|
35
|
+
cidr_block: cidr_block.cidr_ip
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
rule.user_id_group_pairs.each do |source_security_group|
|
40
|
+
@_sg[:ingress] << {
|
41
|
+
protocol: rule.ip_protocol.to_s,
|
42
|
+
from_port: rule.from_port.to_i,
|
43
|
+
to_port: rule.to_port.to_i,
|
44
|
+
source_security_group: source_security_group.group_id
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# EGRESS
|
50
|
+
object.ip_permissions_egress.each do |rule|
|
51
|
+
rule.ip_ranges.each do |cidr_block|
|
52
|
+
@_sg[:egress] << {
|
53
|
+
protocol: rule.ip_protocol.to_s,
|
54
|
+
from_port: rule.from_port.to_i,
|
55
|
+
to_port: rule.to_port.to_i,
|
56
|
+
cidr_block: cidr_block.cidr_ip
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
rule.user_id_group_pairs.each do |source_security_group|
|
61
|
+
@_sg[:egress] << {
|
62
|
+
protocol: rule.ip_protocol.to_s,
|
63
|
+
from_port: rule.from_port.to_i,
|
64
|
+
to_port: rule.to_port.to_i,
|
65
|
+
source_security_group: source_security_group.group_id
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
return true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def load_local(object)
|
74
|
+
if object.is_a?(Hash)
|
75
|
+
@_local_sg[:name] = object['name']
|
76
|
+
@_local_sg[:description] = 'Managed by Demeter'
|
77
|
+
@_local_sg[:ingress] = []
|
78
|
+
@_local_sg[:egress] = []
|
79
|
+
@_local_sg[:vpc_id] = object['vpc_id']
|
80
|
+
|
81
|
+
if !Demeter::vars.has_key?("security_group.#{project_key}.id")
|
82
|
+
Demeter::set_var("security_group.#{project_key}.id", "security_group.#{project_key}.id")
|
83
|
+
end
|
84
|
+
|
85
|
+
# INGRESS
|
86
|
+
if object['ingress']
|
87
|
+
object['ingress'].each do |rule|
|
88
|
+
if rule.has_key?('cidr_blocks')
|
89
|
+
rule['cidr_blocks'].to_a.each do |cidr_block|
|
90
|
+
@_local_sg[:ingress] << {
|
91
|
+
protocol: rule['protocol'].to_s,
|
92
|
+
from_port: rule['from_port'].to_i,
|
93
|
+
to_port: rule['to_port'].to_i,
|
94
|
+
cidr_block: cidr_block
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if rule.has_key?('source_security_groups')
|
100
|
+
rule['source_security_groups'].to_a.each do |source_security_group|
|
101
|
+
@_local_sg[:ingress] << {
|
102
|
+
protocol: rule['protocol'].to_s,
|
103
|
+
from_port: rule['from_port'].to_i,
|
104
|
+
to_port: rule['to_port'].to_i,
|
105
|
+
source_security_group: source_security_group
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# EGRESS
|
113
|
+
if object['egress']
|
114
|
+
object['egress'].each do |rule|
|
115
|
+
if rule.has_key?('cidr_blocks')
|
116
|
+
rule['cidr_blocks'].to_a.each do |cidr_block|
|
117
|
+
@_local_sg[:egress] << {
|
118
|
+
protocol: rule['protocol'].to_s,
|
119
|
+
from_port: rule['from_port'].to_i,
|
120
|
+
to_port: rule['to_port'].to_i,
|
121
|
+
cidr_block: cidr_block
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
if rule.has_key?('source_security_groups')
|
127
|
+
rule['source_security_groups'].to_a.each do |source_security_group|
|
128
|
+
@_local_sg[:egress] << {
|
129
|
+
protocol: rule['protocol'].to_s,
|
130
|
+
from_port: rule['from_port'].to_i,
|
131
|
+
to_port: rule['to_port'].to_i,
|
132
|
+
source_security_group: source_security_group
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
return true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def security_group
|
144
|
+
if @_sg.empty?
|
145
|
+
@_local_sg
|
146
|
+
else
|
147
|
+
@_sg
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def hash
|
152
|
+
group_name
|
153
|
+
end
|
154
|
+
|
155
|
+
def project_key
|
156
|
+
security_group[:name]
|
157
|
+
.gsub('::', '_')
|
158
|
+
.gsub('/', '_')
|
159
|
+
.gsub('-', '_')
|
160
|
+
.gsub(' ', '_')
|
161
|
+
.downcase
|
162
|
+
end
|
163
|
+
|
164
|
+
def project_name
|
165
|
+
security_group[:name].split('::')[1]
|
166
|
+
end
|
167
|
+
|
168
|
+
def group_name
|
169
|
+
security_group[:name]
|
170
|
+
end
|
171
|
+
|
172
|
+
def diff
|
173
|
+
sg = @_sg.select{ |key, value| true if key == :ingress || key == :egress }
|
174
|
+
|
175
|
+
# update variables
|
176
|
+
local_sg = update_vars(@_local_sg.select{ |key, value| true if key == :ingress || key == :egress })
|
177
|
+
# update once again to replace deep variable links
|
178
|
+
local_sg = update_vars(local_sg)
|
179
|
+
|
180
|
+
if sg[:ingress]
|
181
|
+
sg[:ingress].sort_by! { |x| x.to_s }
|
182
|
+
end
|
183
|
+
|
184
|
+
if local_sg[:ingress]
|
185
|
+
local_sg[:ingress].sort_by! { |x| x.to_s }
|
186
|
+
end
|
187
|
+
|
188
|
+
if sg[:egress]
|
189
|
+
sg[:egress].sort_by! { |x| x.to_s }
|
190
|
+
end
|
191
|
+
|
192
|
+
if local_sg[:egress]
|
193
|
+
local_sg[:egress].sort_by! { |x| x.to_s }
|
194
|
+
end
|
195
|
+
diff = HashDiff.diff(sg, local_sg)
|
196
|
+
end
|
197
|
+
|
198
|
+
def update_vars(hash=@_local_sg)
|
199
|
+
hash.each do |k, v|
|
200
|
+
if v.is_a?(String)
|
201
|
+
if /\<\%(.*)\%\>/.match(v)
|
202
|
+
var_keys = /\<\%(.*)\%\>/.match(v).captures
|
203
|
+
if Demeter::vars.has_key?(var_keys[0].strip)
|
204
|
+
hash[k] = Demeter::vars[var_keys[0].strip]
|
205
|
+
if hash[k].is_a?(Array)
|
206
|
+
extended = []
|
207
|
+
hash[k].each do |value|
|
208
|
+
h1 = hash.clone
|
209
|
+
h1[k] = value
|
210
|
+
extended << h1
|
211
|
+
end
|
212
|
+
hash = extended
|
213
|
+
end
|
214
|
+
else
|
215
|
+
fail "Key #{v} not found!"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
elsif v.is_a?(Hash)
|
219
|
+
hash[k] = update_vars v
|
220
|
+
elsif v.is_a?(Array)
|
221
|
+
tmp = []
|
222
|
+
v.flatten.each do |x|
|
223
|
+
if x.is_a?(Hash)
|
224
|
+
tmp << update_vars(x)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
hash[k] = tmp.flatten
|
228
|
+
end
|
229
|
+
end
|
230
|
+
hash
|
231
|
+
end
|
232
|
+
|
233
|
+
def create
|
234
|
+
if @_sg.empty?
|
235
|
+
# update variables
|
236
|
+
local_sg = update_vars(@_local_sg)
|
237
|
+
# update once again to replace deep variable links
|
238
|
+
local_sg = update_vars(@_local_sg)
|
239
|
+
|
240
|
+
resp = @ec2.create_security_group({
|
241
|
+
group_name: local_sg[:name],
|
242
|
+
description: local_sg[:description], # required
|
243
|
+
vpc_id: local_sg[:vpc_id]
|
244
|
+
})
|
245
|
+
|
246
|
+
@ec2.create_tags(:resources => [resp.group_id], :tags => [
|
247
|
+
{ :key => 'Name', :value => local_sg[:name] }
|
248
|
+
])
|
249
|
+
|
250
|
+
puts "Created SG: #{local_sg['name']} (#{resp.group_id})"
|
251
|
+
true
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def modify
|
256
|
+
the_diff = diff
|
257
|
+
pluses = the_diff.select { |s| s[0] == "+" }
|
258
|
+
minuses = the_diff.select { |s| s[0] == "-" }
|
259
|
+
pluses.each do |plus|
|
260
|
+
next if plus[1] == "description"
|
261
|
+
values = plus[2]
|
262
|
+
if values.has_key?(:cidr_block)
|
263
|
+
if plus[1].include?('ingress')
|
264
|
+
@ec2.authorize_security_group_ingress({
|
265
|
+
group_id: group_id,
|
266
|
+
ip_protocol: values[:protocol],
|
267
|
+
from_port: values[:from_port],
|
268
|
+
to_port: values[:to_port],
|
269
|
+
cidr_ip: values[:cidr_block],
|
270
|
+
})
|
271
|
+
else
|
272
|
+
@ec2.authorize_security_group_egress({
|
273
|
+
group_id: group_id,
|
274
|
+
ip_permissions: [{
|
275
|
+
ip_protocol: values[:protocol],
|
276
|
+
from_port: values[:from_port],
|
277
|
+
to_port: values[:to_port],
|
278
|
+
ip_ranges: [
|
279
|
+
{
|
280
|
+
cidr_ip: values[:cidr_block]
|
281
|
+
}
|
282
|
+
]
|
283
|
+
}]
|
284
|
+
})
|
285
|
+
end
|
286
|
+
elsif values.has_key?(:source_security_group)
|
287
|
+
if plus[1].include?('ingress')
|
288
|
+
@ec2.authorize_security_group_ingress({
|
289
|
+
group_id: group_id,
|
290
|
+
ip_permissions: [{
|
291
|
+
ip_protocol: values[:protocol],
|
292
|
+
from_port: values[:from_port],
|
293
|
+
to_port: values[:to_port],
|
294
|
+
user_id_group_pairs: [{
|
295
|
+
group_id: values[:source_security_group]
|
296
|
+
}]
|
297
|
+
}]
|
298
|
+
})
|
299
|
+
else
|
300
|
+
@ec2.authorize_security_group_egress({
|
301
|
+
group_id: group_id,
|
302
|
+
ip_permissions: [{
|
303
|
+
ip_protocol: values[:protocol],
|
304
|
+
from_port: values[:from_port],
|
305
|
+
to_port: values[:to_port],
|
306
|
+
user_id_group_pairs: [{
|
307
|
+
group_id: values[:source_security_group]
|
308
|
+
}]
|
309
|
+
}]
|
310
|
+
})
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
minuses.each do |minus|
|
315
|
+
values = minus[2]
|
316
|
+
if values.has_key?(:cidr_block)
|
317
|
+
if minus[1].include?('ingress')
|
318
|
+
@ec2.revoke_security_group_ingress({
|
319
|
+
group_id: group_id,
|
320
|
+
ip_protocol: values[:protocol],
|
321
|
+
from_port: values[:from_port],
|
322
|
+
to_port: values[:to_port],
|
323
|
+
cidr_ip: values[:cidr_block],
|
324
|
+
})
|
325
|
+
else
|
326
|
+
@ec2.revoke_security_group_egress({
|
327
|
+
group_id: group_id,
|
328
|
+
ip_permissions: [{
|
329
|
+
ip_protocol: values[:protocol],
|
330
|
+
from_port: values[:from_port],
|
331
|
+
to_port: values[:to_port],
|
332
|
+
ip_ranges: [
|
333
|
+
{
|
334
|
+
cidr_ip: values[:cidr_block]
|
335
|
+
}
|
336
|
+
]
|
337
|
+
}]
|
338
|
+
})
|
339
|
+
end
|
340
|
+
elsif values.has_key?(:source_security_group)
|
341
|
+
if minus[1].include?('ingress')
|
342
|
+
@ec2.revoke_security_group_ingress({
|
343
|
+
group_id: group_id,
|
344
|
+
ip_permissions: [{
|
345
|
+
ip_protocol: values[:protocol],
|
346
|
+
from_port: values[:from_port],
|
347
|
+
to_port: values[:to_port],
|
348
|
+
user_id_group_pairs: [{
|
349
|
+
group_id: values[:source_security_group]
|
350
|
+
}]
|
351
|
+
}]
|
352
|
+
})
|
353
|
+
else
|
354
|
+
@ec2.revoke_security_group_egress({
|
355
|
+
group_id: group_id,
|
356
|
+
ip_permissions: [{
|
357
|
+
ip_protocol: values[:protocol],
|
358
|
+
from_port: values[:from_port],
|
359
|
+
to_port: values[:to_port],
|
360
|
+
user_id_group_pairs: [{
|
361
|
+
group_id: values[:source_security_group]
|
362
|
+
}]
|
363
|
+
}]
|
364
|
+
})
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
data/lib/demeter/cli.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Demeter
|
4
|
+
class Cli < Thor
|
5
|
+
# include Thor::Actions
|
6
|
+
|
7
|
+
option :debug, desc: 'displays the debug backtrace', type: :boolean, default: false
|
8
|
+
def initialize(args = [], local_options = {}, config = {})
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'version', 'prints Demeter version'
|
13
|
+
long_desc <<-EOS
|
14
|
+
`demeter version` prints the version of the app.
|
15
|
+
EOS
|
16
|
+
def version
|
17
|
+
puts "v#{Demeter::VERSION}"
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
desc 'status', 'Show current status of maneged and unmaneged security groups'
|
22
|
+
long_desc <<-EOS
|
23
|
+
`demeter status` shows current status of managed and unmaneged security groups.
|
24
|
+
|
25
|
+
$ > demeter plan -e development
|
26
|
+
EOS
|
27
|
+
option :environment, aliases: '-e', :required => true, desc: 'The environment to plan against'
|
28
|
+
def status
|
29
|
+
if options[:help]
|
30
|
+
invoke :help, ['status']
|
31
|
+
else
|
32
|
+
require 'demeter/commands/status'
|
33
|
+
Demeter::Commands::Status.new(options).start
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
desc 'plan', 'Generate and show an execution plan'
|
39
|
+
long_desc <<-EOS
|
40
|
+
`demeter plan` generates and shows the execution plan.
|
41
|
+
|
42
|
+
$ > demeter plan -e development
|
43
|
+
EOS
|
44
|
+
option :environment, aliases: '-e', :required => true, desc: 'The environment to plan against'
|
45
|
+
def plan
|
46
|
+
if options[:help]
|
47
|
+
invoke :help, ['plan']
|
48
|
+
else
|
49
|
+
require 'demeter/commands/plan'
|
50
|
+
Demeter::Commands::Plan.new(options).start
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
desc 'apply', 'Apply an execution plan'
|
56
|
+
long_desc <<-EOS
|
57
|
+
`demeter apply` applies the execution plan.
|
58
|
+
|
59
|
+
$ > demeter apply -e development
|
60
|
+
EOS
|
61
|
+
option :environment, aliases: '-e', :required => true, desc: 'The environment to plan against'
|
62
|
+
def apply
|
63
|
+
if options[:help]
|
64
|
+
invoke :help, ['apply']
|
65
|
+
else
|
66
|
+
require 'demeter/commands/apply'
|
67
|
+
Demeter::Commands::Apply.new(options).start
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
desc 'generate', 'Generate local config from aws describe call'
|
73
|
+
long_desc <<-EOS
|
74
|
+
`demeter generate -e development -ids <sg-id> ...`
|
75
|
+
|
76
|
+
$ > demeter generate -e development -ids sg-000000 sg-111111
|
77
|
+
|
78
|
+
$ > demeter generate -e development -ids sg-000000
|
79
|
+
EOS
|
80
|
+
option :environment, aliases: '-e', :required => true, desc: 'The environment to plan against'
|
81
|
+
option :ids, aliases: '-ids', type: :array, :required => true, desc: 'List of security group ids (sg-000000)'
|
82
|
+
def generate
|
83
|
+
if options[:help]
|
84
|
+
invoke :help, ['generate']
|
85
|
+
else
|
86
|
+
require 'demeter/commands/generate'
|
87
|
+
Demeter::Commands::Generate.new(options).start
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|