stacco 0.1.10 → 0.1.17
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Dockerfile +10 -0
- data/README.md +5 -0
- data/bin/stacco +126 -17
- data/lib/aws/with-stacco-patches.rb +149 -0
- data/lib/stacco.rb +4 -418
- data/lib/stacco/base.rb +2 -0
- data/lib/stacco/orchestrator.rb +43 -0
- data/lib/stacco/resources.rb +25 -0
- data/lib/stacco/stack.rb +195 -0
- data/lib/stacco/template.rb +209 -0
- data/lib/stacco/template/base.rb +67 -0
- data/lib/stacco/template/old.rb +85 -0
- data/priv/cloud-init/common.after.sh +2 -0
- data/priv/cloud-init/common.before.sh +11 -9
- data/priv/cloud-init/roles/vpn.sh +16 -10
- data/priv/templates/{cloudformation.json.erb.with-proxy-protocol → bexng.rb} +1 -24
- data/priv/templates/cloudformation.json.erb +614 -380
- data/stacco.gemspec +2 -1
- metadata +403 -7
data/.gitignore
CHANGED
data/Dockerfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
FROM tsutsu/ubuntu:latest
|
2
|
+
|
3
|
+
RUN /lib/container/do-with-clean <<EOF \
|
4
|
+
apt-get update \
|
5
|
+
apt-get install -fqy openssh-client ruby2.0 bundler \
|
6
|
+
gem install -y --no-rdoc --no-ri stacco \
|
7
|
+
EOF
|
8
|
+
|
9
|
+
WORKDIR /var/lib/stacco
|
10
|
+
ENTRYPOINT ["/usr/local/bin/stacco"]
|
data/README.md
ADDED
data/bin/stacco
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
4
|
+
|
3
5
|
require 'stacco'
|
4
6
|
require 'pathname'
|
7
|
+
require 'highline'
|
8
|
+
require 'inifile'
|
5
9
|
|
6
10
|
class String
|
7
11
|
def colored(color)
|
@@ -55,26 +59,81 @@ unless ARGV.length >= 1
|
|
55
59
|
Kernel.exit 1
|
56
60
|
end
|
57
61
|
|
62
|
+
console = HighLine.new
|
63
|
+
|
58
64
|
subcommand = ARGV.shift.intern
|
59
|
-
|
65
|
+
|
66
|
+
orch_config = {
|
67
|
+
local: Pathname.new('orchestrator.yml'),
|
68
|
+
user: Pathname.new(ENV['HOME']) + '.config' + 'stacco' + 'orchestrator.yml'
|
69
|
+
}
|
70
|
+
orch_config = orch_config[:local].file? ? orch_config[:local] : orch_config[:user]
|
71
|
+
orch_config.parent.mkpath
|
60
72
|
|
61
73
|
if subcommand == :init
|
62
|
-
|
63
|
-
|
74
|
+
if $stdin.tty?
|
75
|
+
new_orch_config = {aws: {}}
|
76
|
+
|
77
|
+
aws_config = Pathname.new(ENV['HOME']) + '.aws' + 'config'
|
78
|
+
if aws_config.file?
|
79
|
+
aws_config = IniFile.new(filename: aws_config.to_s)
|
80
|
+
aws_profiles = aws_config.sections - ["preview"]
|
81
|
+
|
82
|
+
if aws_profiles.length > 1
|
83
|
+
aws_profile = console.choose(aws_profiles) do |menu|
|
84
|
+
menu.prompt = "Local AWS profile to source: "
|
85
|
+
end
|
86
|
+
else
|
87
|
+
console.say "Sourcing AWS config from local AWS profile."
|
88
|
+
aws_profile = aws_profiles.first
|
89
|
+
end
|
90
|
+
|
91
|
+
new_orch_config[:aws][:access_key_id] = aws_config[aws_profile]['aws_access_key_id']
|
92
|
+
new_orch_config[:aws][:secret_access_key] = aws_config[aws_profile]['aws_secret_access_key']
|
93
|
+
new_orch_config[:aws][:region] = aws_config[aws_profile]['region']
|
94
|
+
end
|
95
|
+
|
96
|
+
new_orch_config[:aws][:access_key_id] = console.ask("AWS account ID: ") do |question|
|
97
|
+
question.default = new_orch_config[:aws][:access_key_id]
|
98
|
+
question.validate = /^[0-9A-Z]{20}$/
|
99
|
+
end
|
100
|
+
|
101
|
+
new_orch_config[:aws][:secret_access_key] = console.ask("AWS account secret: ") do |question|
|
102
|
+
question.default = new_orch_config[:aws][:secret_access_key]
|
103
|
+
question.validate = /^[=+\/0-9A-Za-z]{32,64}$/
|
104
|
+
end
|
105
|
+
|
106
|
+
new_orch_config[:aws][:region] = console.choose(*(AWS.regions.map{ |r| r.name }.sort)) do |menu|
|
107
|
+
menu.prompt = "Orchestrator storage-bucket region: "
|
108
|
+
menu.default = new_orch_config[:aws][:region]
|
109
|
+
menu.layout = :one_line
|
110
|
+
menu.select_by = :name_or_index
|
111
|
+
end
|
112
|
+
|
113
|
+
new_orch_config[:storage_bucket] = console.ask("Orchestrator storage-bucket name: ") do |question|
|
114
|
+
question.validate = /^[A-Za-z][-.0-9A-Za-z]+[A-Za-z0-9]$/
|
115
|
+
end
|
116
|
+
|
117
|
+
# convert symbol-keys to string-keys
|
118
|
+
new_orch_config = JSON.parse(new_orch_config.to_json)
|
119
|
+
else
|
120
|
+
new_orch_config = YAML.load($stdin.read)
|
64
121
|
end
|
65
122
|
|
66
|
-
|
123
|
+
orch_config.open('w'){ |f| f.write(new_orch_config.to_yaml) }
|
124
|
+
|
125
|
+
orch = Stacco::Orchestrator.from_config(orch_config)
|
67
126
|
puts "orchestrator initialized"
|
68
127
|
Kernel.exit 0
|
69
128
|
end
|
70
129
|
|
71
|
-
unless
|
130
|
+
unless orch_config.file?
|
72
131
|
$stderr.puts "orchestrator not initialized!"
|
73
132
|
$stderr.puts "Did you run 'stacco init'?"
|
74
133
|
Kernel.exit 1
|
75
134
|
end
|
76
135
|
|
77
|
-
orch = Stacco::Orchestrator.from_config(
|
136
|
+
orch = Stacco::Orchestrator.from_config(orch_config)
|
78
137
|
stacks = orch.stacks
|
79
138
|
|
80
139
|
case subcommand
|
@@ -132,9 +191,29 @@ when :edit
|
|
132
191
|
stack.config = YAML.load(tmp_stackdef_path.read)
|
133
192
|
|
134
193
|
|
135
|
-
when :"bake
|
194
|
+
when :"template:bake"
|
136
195
|
puts stack.cloudformation_template
|
137
196
|
|
197
|
+
when :"template:validate"
|
198
|
+
success, err, pos = stack.validate
|
199
|
+
if success
|
200
|
+
puts "template is valid"
|
201
|
+
else
|
202
|
+
$stderr.puts err.colored(:red)
|
203
|
+
if pos
|
204
|
+
$stderr.puts
|
205
|
+
line, column = pos
|
206
|
+
line_before, line_after = line[0...column], line[column..-1]
|
207
|
+
if line_after
|
208
|
+
token_at, rest_of_line = line_after.split(' ', 2)
|
209
|
+
$stderr.puts line_before + (token_at || '').colored(:red) + (rest_of_line || '')
|
210
|
+
else
|
211
|
+
$stderr.puts line_before + ' #'.colored(:blue) + ' <-'.colored(:red)
|
212
|
+
end
|
213
|
+
$stderr.puts
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
138
217
|
when :repl
|
139
218
|
Stack = stack
|
140
219
|
require 'irb'
|
@@ -146,9 +225,12 @@ when :"cloudfront:init"
|
|
146
225
|
when :connect
|
147
226
|
conns = stack.connections
|
148
227
|
|
228
|
+
bastion_host = conns.find{|k,v| k =~ /bastion/i and v.dns_name }[1]
|
229
|
+
|
149
230
|
unless host_logicalname = ARGV.shift
|
150
|
-
conns.each do |resource_name,
|
151
|
-
|
231
|
+
conns.each do |resource_name, inst|
|
232
|
+
connection_path = inst.dns_name ? inst.dns_name : ("%s -> %s" % [bastion_host.dns_name, inst.private_dns_name])
|
233
|
+
puts "#{resource_name}: #{connection_path}"
|
152
234
|
end
|
153
235
|
|
154
236
|
Kernel.exit 0
|
@@ -158,18 +240,45 @@ when :connect
|
|
158
240
|
$stderr.puts "#{host_logicalname}: not available"
|
159
241
|
Kernel.exit 1
|
160
242
|
end
|
161
|
-
use_eip = conns[host_logicalname]
|
162
243
|
|
163
|
-
|
244
|
+
connect_to_host = conns[host_logicalname]
|
245
|
+
|
246
|
+
private_key_path = Pathname.new("id_rsa").expand_path
|
164
247
|
private_key_path.open('wb'){ |f| f.write(stack.iam_private_key_material) }
|
165
248
|
private_key_path.chmod(0600)
|
166
249
|
|
167
|
-
|
168
|
-
|
169
|
-
'-
|
170
|
-
|
171
|
-
|
172
|
-
|
250
|
+
|
251
|
+
begin
|
252
|
+
Kernel.system 'ssh-add', private_key_path.to_s
|
253
|
+
if connect_to_host.dns_name
|
254
|
+
Kernel.system 'ssh', '-A', '-tt', '-v',
|
255
|
+
'-i', private_key_path.to_s,
|
256
|
+
'-o', 'StrictHostKeyChecking=no',
|
257
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
258
|
+
"ubuntu@#{connect_to_host.dns_name}",
|
259
|
+
'sudo', 'su', '-'
|
260
|
+
else
|
261
|
+
Kernel.system 'ssh', '-A', '-tt', '-v',
|
262
|
+
'-o', 'StrictHostKeyChecking=no',
|
263
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
264
|
+
"ubuntu@#{bastion_host.dns_name}",
|
265
|
+
'ssh', '-A', '-tt', '-v',
|
266
|
+
'-o', 'StrictHostKeyChecking=no',
|
267
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
268
|
+
"ubuntu@#{connect_to_host.private_dns_name}",
|
269
|
+
'sudo', 'su', '-'
|
270
|
+
end
|
271
|
+
ensure
|
272
|
+
Kernel.system 'ssh-add', '-d', private_key_path.to_s
|
273
|
+
end
|
274
|
+
|
275
|
+
when :"status:wait"
|
276
|
+
desired_status = ARGV.shift
|
277
|
+
if desired_status == 'down'
|
278
|
+
Kernel.sleep(2) while stack.up?
|
279
|
+
else
|
280
|
+
Kernel.sleep(2) until stack.up? and stack.aws_status == desired_status
|
281
|
+
end
|
173
282
|
|
174
283
|
when :status
|
175
284
|
stack.must_be_up!
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'aws'
|
2
|
+
|
3
|
+
class AWS::CloudFormation::StackEvent
|
4
|
+
def operation
|
5
|
+
self.resource_status.split('_', 2).first
|
6
|
+
end
|
7
|
+
|
8
|
+
def status
|
9
|
+
stat = self.resource_status.split('_', 2).last
|
10
|
+
return "started" if (self.resource_type == "AWS::CloudFormation::Stack" and stat == "IN_PROGRESS")
|
11
|
+
return "WORKING" if stat == "IN_PROGRESS"
|
12
|
+
stat
|
13
|
+
end
|
14
|
+
|
15
|
+
def error
|
16
|
+
self.resource_status_reason if (self.resource_status_reason and self.resource_status_reason !~ / Initiated$/)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class AWS::CloudFormation::Stack
|
21
|
+
attr_accessor :service_registry
|
22
|
+
|
23
|
+
def resources_of_type(type_name)
|
24
|
+
self.resources.find_all{ |r| r.resource_type == "AWS::#{type_name}" && r.resource_status =~ /_COMPLETE/ && r.resource_status != "DELETE_COMPLETE" }
|
25
|
+
end
|
26
|
+
|
27
|
+
def distributions
|
28
|
+
self.resources_of_type("CloudFront::Distribution").map{ |res| @service_registry[:cloudfront].distributions[res.physical_resource_id] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def buckets
|
32
|
+
self.resources_of_type("S3::Bucket").map{ |res| @service_registry[:s3].buckets[res.physical_resource_id] }.find_all{ |r| r.exists? }
|
33
|
+
end
|
34
|
+
|
35
|
+
def instances
|
36
|
+
self.resources_of_type("EC2::Instance").map{ |res| @service_registry[:ec2].instances[res.physical_resource_id] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def elastic_ips
|
40
|
+
self.resources_of_type("EC2::EIP").map{ |res| @service_registry[:ec2].elastic_ips[res.physical_resource_id] }
|
41
|
+
end
|
42
|
+
|
43
|
+
def server_certificates(opts = {})
|
44
|
+
certs = @service_registry[:iam].server_certificates.find_all{ |cert| cert.path == "/cloudfront/#{self.name}/" }
|
45
|
+
|
46
|
+
if cnames = opts.delete(:domain)
|
47
|
+
certs = Hash[ *(certs.map{ |cert| [cert.name, cert] }.flatten) ]
|
48
|
+
|
49
|
+
cnames = [cnames] unless cnames.kind_of?(Array)
|
50
|
+
cnames = cnames.flatten
|
51
|
+
|
52
|
+
possible_cert_names = cnames.map{ |cname| parts = cname.split('.').reverse; (1..parts.length).map{ |len| parts[0, len].reverse.join('.') }.reverse }.flatten
|
53
|
+
|
54
|
+
possible_cert_names.map{ |cname| certs[cname] }.compact
|
55
|
+
else
|
56
|
+
certs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class AWS::CloudFront
|
62
|
+
def distributions
|
63
|
+
DistributionCollection.new(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
class DistributionCollection
|
67
|
+
def initialize(svc)
|
68
|
+
@svc = svc
|
69
|
+
end
|
70
|
+
|
71
|
+
def [](k)
|
72
|
+
Distribution.new(@svc, @svc.client.get_distribution(id: k))
|
73
|
+
end
|
74
|
+
|
75
|
+
def each
|
76
|
+
dists = @svc.client.list_distributions[:items]
|
77
|
+
dists.each do |dist|
|
78
|
+
yield Distribution.new(@svc, @svc.client.get_distribution(id: dist[:id]))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
include Enumerable
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class AWS::CloudFront::Distribution
|
87
|
+
def initialize(svc, data)
|
88
|
+
@svc = svc
|
89
|
+
@data = data
|
90
|
+
end
|
91
|
+
|
92
|
+
def aliases
|
93
|
+
@data[:distribution_config][:aliases][:items]
|
94
|
+
end
|
95
|
+
|
96
|
+
def id
|
97
|
+
@data[:id]
|
98
|
+
end
|
99
|
+
|
100
|
+
def config
|
101
|
+
self.make_exportable(@data[:distribution_config])
|
102
|
+
end
|
103
|
+
|
104
|
+
def make_exportable(o)
|
105
|
+
case o
|
106
|
+
when nil
|
107
|
+
""
|
108
|
+
when Hash
|
109
|
+
h = {}
|
110
|
+
o.each{ |k, v| h[k] = make_exportable(v) }
|
111
|
+
h
|
112
|
+
when Array
|
113
|
+
o.map{ |e| make_exportable(e) }
|
114
|
+
else
|
115
|
+
o
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def update
|
120
|
+
begin
|
121
|
+
yield
|
122
|
+
|
123
|
+
@svc.client.update_distribution(
|
124
|
+
id: self.id,
|
125
|
+
distribution_config: self.config,
|
126
|
+
if_match: @data[:etag]
|
127
|
+
)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def price_class
|
132
|
+
@data[:distribution_config][:price_class].split('_').last.downcase.intern
|
133
|
+
end
|
134
|
+
|
135
|
+
def price_class=(new_class)
|
136
|
+
@data[:distribution_config][:price_class] = "PriceClass_#{new_class.to_s.capitalize}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def certificate
|
140
|
+
@data[:distribution_config][:viewer_certificate][:iam_certificate_id]
|
141
|
+
end
|
142
|
+
|
143
|
+
def certificate=(cert_id)
|
144
|
+
@data[:distribution_config][:viewer_certificate] = {
|
145
|
+
iam_certificate_id: cert_id,
|
146
|
+
ssl_support_method: "sni-only"
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
data/lib/stacco.rb
CHANGED
@@ -1,418 +1,4 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require 'shellwords'
|
6
|
-
|
7
|
-
require 'json'
|
8
|
-
require 'yaml'
|
9
|
-
require 'erb'
|
10
|
-
require 'inifile'
|
11
|
-
|
12
|
-
require 'aws'
|
13
|
-
|
14
|
-
class AWS::CloudFormation::StackEvent
|
15
|
-
def operation
|
16
|
-
self.resource_status.split('_', 2).first
|
17
|
-
end
|
18
|
-
|
19
|
-
def status
|
20
|
-
stat = self.resource_status.split('_', 2).last
|
21
|
-
return "started" if (self.resource_type == "AWS::CloudFormation::Stack" and stat == "IN_PROGRESS")
|
22
|
-
return "WORKING" if stat == "IN_PROGRESS"
|
23
|
-
stat
|
24
|
-
end
|
25
|
-
|
26
|
-
def error
|
27
|
-
self.resource_status_reason if (self.resource_status_reason and self.resource_status_reason !~ / Initiated$/)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class AWS::CloudFormation::Stack
|
32
|
-
attr_accessor :service_registry
|
33
|
-
|
34
|
-
def resources_of_type(type_name)
|
35
|
-
self.resources.find_all{ |r| r.resource_type == "AWS::#{type_name}" && r.resource_status =~ /_COMPLETE/ && r.resource_status != "DELETE_COMPLETE" }
|
36
|
-
end
|
37
|
-
|
38
|
-
def distributions
|
39
|
-
self.resources_of_type("CloudFront::Distribution").map{ |res| @service_registry[:cloudfront].distributions[res.physical_resource_id] }
|
40
|
-
end
|
41
|
-
|
42
|
-
def buckets
|
43
|
-
self.resources_of_type("S3::Bucket").map{ |res| @service_registry[:s3].buckets[res.physical_resource_id] }.find_all{ |r| r.exists? }
|
44
|
-
end
|
45
|
-
|
46
|
-
def elastic_ips
|
47
|
-
self.resources_of_type("EC2::EIP").map{ |res| @service_registry[:ec2].elastic_ips[res.physical_resource_id] }
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class AWS::CloudFront
|
52
|
-
def distributions
|
53
|
-
self.client.list_distributions[:items].map{ |dist| Distribution.new(self.client, self.client.get_distribution(id: dist[:id])) }
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
class AWS::CloudFront::Distribution
|
58
|
-
def initialize(client, data)
|
59
|
-
@client = client
|
60
|
-
@data = data
|
61
|
-
end
|
62
|
-
|
63
|
-
def aliases
|
64
|
-
@data[:distribution_config][:aliases][:items]
|
65
|
-
end
|
66
|
-
|
67
|
-
def id
|
68
|
-
@data[:id]
|
69
|
-
end
|
70
|
-
|
71
|
-
def config
|
72
|
-
self.make_exportable(@data[:distribution_config])
|
73
|
-
end
|
74
|
-
|
75
|
-
def make_exportable(o)
|
76
|
-
case o
|
77
|
-
when nil
|
78
|
-
""
|
79
|
-
when Hash
|
80
|
-
h = {}
|
81
|
-
o.each{ |k, v| h[k] = make_exportable(v) }
|
82
|
-
h
|
83
|
-
when Array
|
84
|
-
o.map{ |e| make_exportable(e) }
|
85
|
-
else
|
86
|
-
o
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def update
|
91
|
-
begin
|
92
|
-
yield
|
93
|
-
|
94
|
-
@client.update_distribution(
|
95
|
-
id: self.id,
|
96
|
-
distribution_config: self.config,
|
97
|
-
if_match: @data[:etag]
|
98
|
-
)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def price_class
|
103
|
-
@data[:distribution_config][:price_class].split('_').last.downcase.intern
|
104
|
-
end
|
105
|
-
|
106
|
-
def price_class=(new_class)
|
107
|
-
@data[:distribution_config][:price_class] = "PriceClass_#{new_class.to_s.capitalize}"
|
108
|
-
end
|
109
|
-
|
110
|
-
def certificate
|
111
|
-
@data[:distribution_config][:viewer_certificate][:iam_certificate_id]
|
112
|
-
end
|
113
|
-
|
114
|
-
def certificate=(cert_id)
|
115
|
-
@data[:distribution_config][:viewer_certificate] = {
|
116
|
-
iam_certificate_id: cert_id,
|
117
|
-
ssl_support_method: "sni-only"
|
118
|
-
}
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
module Stacco
|
123
|
-
module Resources
|
124
|
-
RootDir = Pathname.new(File.expand_path("../../priv", __FILE__))
|
125
|
-
|
126
|
-
templates_dir = Stacco::Resources::RootDir + "templates"
|
127
|
-
Templates = {
|
128
|
-
cloudformation: (templates_dir + "cloudformation.json.erb").read
|
129
|
-
}
|
130
|
-
|
131
|
-
module CloudInit
|
132
|
-
cloud_init_scripts_dir = Stacco::Resources::RootDir + "cloud-init"
|
133
|
-
|
134
|
-
CommonScripts = {
|
135
|
-
before: (cloud_init_scripts_dir + "common.before.sh").readlines,
|
136
|
-
after: (cloud_init_scripts_dir + "common.after.sh").readlines
|
137
|
-
}
|
138
|
-
|
139
|
-
RoleScripts = {}
|
140
|
-
(cloud_init_scripts_dir + "roles").children.each{ |path| RoleScripts[path.basename('.sh').to_s.intern] = path.readlines }
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
class Stacco::Orchestrator
|
146
|
-
def self.from_config(config_body)
|
147
|
-
config_body = config_body.read if config_body.respond_to?(:read)
|
148
|
-
self.new YAML.load(config_body)
|
149
|
-
end
|
150
|
-
|
151
|
-
def initialize(config)
|
152
|
-
@config = config
|
153
|
-
|
154
|
-
storage_bucket_name = @config['storage_bucket']
|
155
|
-
|
156
|
-
s3 = AWS::S3.new(
|
157
|
-
access_key_id: @config['aws']['access_key_id'],
|
158
|
-
secret_access_key: @config['aws']['secret_access_key'],
|
159
|
-
region: @config['aws']['region']
|
160
|
-
)
|
161
|
-
|
162
|
-
@bucket = s3.buckets[storage_bucket_name]
|
163
|
-
s3.buckets.create(storage_bucket_name) unless @bucket.exists?
|
164
|
-
end
|
165
|
-
|
166
|
-
def stacks
|
167
|
-
@bucket.objects.with_prefix('stack/').to_a[1..-1].map{ |obj| Stacco::Stack.new(obj) }
|
168
|
-
end
|
169
|
-
|
170
|
-
def define_stack(config_body)
|
171
|
-
config_body = config_body.read if config_body.respond_to?(:read)
|
172
|
-
|
173
|
-
config = YAML.load(config_body)
|
174
|
-
|
175
|
-
stack_name = config['name']
|
176
|
-
|
177
|
-
stack_object = @bucket.objects["stack/#{stack_name}"]
|
178
|
-
stack_object.write(config.to_yaml)
|
179
|
-
|
180
|
-
Stacco::Stack.new(stack_object)
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
class Stacco::Stack
|
185
|
-
def initialize(config_object)
|
186
|
-
@config_object = config_object
|
187
|
-
|
188
|
-
aws_creds = self.aws_credentials
|
189
|
-
|
190
|
-
@services = {
|
191
|
-
ec2: AWS::EC2.new(aws_creds),
|
192
|
-
s3: AWS::S3.new(aws_creds),
|
193
|
-
cloudformation: AWS::CloudFormation.new(aws_creds),
|
194
|
-
cloudfront: AWS::CloudFront.new(aws_creds)
|
195
|
-
}
|
196
|
-
|
197
|
-
@aws_stack = @services[:cloudformation].stacks[self.name]
|
198
|
-
@aws_stack.service_registry = @services
|
199
|
-
end
|
200
|
-
|
201
|
-
def connections
|
202
|
-
connections = {}
|
203
|
-
@aws_stack.elastic_ips.each{ |eip| connections[eip.instance.tags["aws:cloudformation:logical-id"]] = eip }
|
204
|
-
connections
|
205
|
-
end
|
206
|
-
|
207
|
-
def resource_summaries
|
208
|
-
@aws_stack.resource_summaries
|
209
|
-
end
|
210
|
-
|
211
|
-
def must_be_up!
|
212
|
-
unless self.up?
|
213
|
-
$stderr.puts "stack #{self.name} is down"
|
214
|
-
Kernel.exit 1
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
def aws_status
|
219
|
-
@aws_stack.status
|
220
|
-
end
|
221
|
-
|
222
|
-
def status
|
223
|
-
self.up? ? self.aws_status : "DOWN"
|
224
|
-
end
|
225
|
-
|
226
|
-
def config
|
227
|
-
YAML.load(@config_object.read)
|
228
|
-
end
|
229
|
-
|
230
|
-
def config=(new_config)
|
231
|
-
@config_object.write(new_config.to_yaml)
|
232
|
-
end
|
233
|
-
|
234
|
-
def update_config
|
235
|
-
# TODO
|
236
|
-
end
|
237
|
-
|
238
|
-
def aws_credentials
|
239
|
-
Hash[ *(self.config['aws'].map{ |k, v| [k.intern, v] }.flatten) ]
|
240
|
-
end
|
241
|
-
|
242
|
-
def description
|
243
|
-
self.config['description']
|
244
|
-
end
|
245
|
-
|
246
|
-
def name
|
247
|
-
self.config['name']
|
248
|
-
end
|
249
|
-
|
250
|
-
def name=(new_name)
|
251
|
-
update_config{ |config| config.merge("name" => new_name) }
|
252
|
-
end
|
253
|
-
|
254
|
-
def up?
|
255
|
-
@aws_stack.exists?
|
256
|
-
end
|
257
|
-
|
258
|
-
def up!
|
259
|
-
if @aws_stack.exists?
|
260
|
-
@aws_stack.update(template: self.cloudformation_template)
|
261
|
-
else
|
262
|
-
@services[:cloudformation].stacks.create(self.name, self.cloudformation_template)
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
def up_since
|
267
|
-
@aws_stack.creation_time if @aws_stack.exists?
|
268
|
-
end
|
269
|
-
|
270
|
-
def initialize_distributions!
|
271
|
-
cloudfront_certs = self.config['cloudfront']['certificates']
|
272
|
-
@services[:cloudfront].distributions.each do |dist|
|
273
|
-
dist.update do
|
274
|
-
dist.price_class = :"100"
|
275
|
-
dist.certificate = cloudfront_certs[dist.aliases.first]
|
276
|
-
end
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
def down!
|
281
|
-
return false unless self.up?
|
282
|
-
|
283
|
-
@aws_stack.buckets.each{ |bucket| bucket.delete! }
|
284
|
-
@aws_stack.delete
|
285
|
-
|
286
|
-
Kernel.sleep(2) while self.up?
|
287
|
-
|
288
|
-
true
|
289
|
-
end
|
290
|
-
|
291
|
-
def cloudformation_template
|
292
|
-
tpl = Stacco::Template.new(self, Stacco::Resources::Templates[:cloudformation])
|
293
|
-
tpl.result(self.config)
|
294
|
-
end
|
295
|
-
|
296
|
-
def iam_private_key
|
297
|
-
@config_object.bucket.objects.with_prefix("sshkey/#{self.name}-").to_a.sort_by{ |obj| obj.key.split('/').last.split('-').last.to_i }.last
|
298
|
-
end
|
299
|
-
|
300
|
-
def iam_keypair_name
|
301
|
-
"stacco-" + self.iam_private_key.key.split('/').last
|
302
|
-
end
|
303
|
-
|
304
|
-
def iam_private_key_material
|
305
|
-
self.iam_private_key.read
|
306
|
-
end
|
307
|
-
|
308
|
-
def stream_events
|
309
|
-
Enumerator.new do |out|
|
310
|
-
known_events = Set.new
|
311
|
-
ticks_without_add = 0
|
312
|
-
|
313
|
-
while self.up?
|
314
|
-
added = 0
|
315
|
-
|
316
|
-
@aws_stack.events.sort_by{ |ev| ev.timestamp }.each do |event|
|
317
|
-
next if known_events.include? event.event_id
|
318
|
-
out.yield event
|
319
|
-
|
320
|
-
known_events.add event.event_id
|
321
|
-
added += 1
|
322
|
-
ticks_without_add = 0
|
323
|
-
|
324
|
-
end
|
325
|
-
|
326
|
-
ticks_without_add += 1 if added == 0
|
327
|
-
|
328
|
-
if ticks_without_add >= 8 and (Math.log2(ticks_without_add) % 1) == 0.0
|
329
|
-
jobs = @aws_stack.resource_summaries
|
330
|
-
active_jobs = jobs.find_all{ |job| job[:resource_status] =~ /IN_PROGRESS$/ }.map{ |job| job[:logical_resource_id] }.sort
|
331
|
-
unless active_jobs.empty?
|
332
|
-
out.yield OpenStruct.new(
|
333
|
-
logical_resource_id: "Scheduler",
|
334
|
-
status: "WAIT",
|
335
|
-
operation: "WAIT",
|
336
|
-
timestamp: Time.now,
|
337
|
-
error: "waiting on #{active_jobs.join(', ')}"
|
338
|
-
)
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
Kernel.sleep 2
|
343
|
-
end
|
344
|
-
end
|
345
|
-
end
|
346
|
-
end
|
347
|
-
|
348
|
-
class Stacco::Template
|
349
|
-
class RenderContext < OpenStruct
|
350
|
-
def initialize(stack, config, vars)
|
351
|
-
@stack = stack
|
352
|
-
@config = config
|
353
|
-
super(vars)
|
354
|
-
end
|
355
|
-
|
356
|
-
def j(str)
|
357
|
-
str.to_json
|
358
|
-
end
|
359
|
-
|
360
|
-
def ja(indent_n, lns)
|
361
|
-
indent = " " * indent_n
|
362
|
-
newline = "\n".to_json
|
363
|
-
lns.map do |ln|
|
364
|
-
if ln.chomp.empty?
|
365
|
-
"%s \n" % [indent]
|
366
|
-
else
|
367
|
-
"%s%s, %s,\n" % [indent, ln.chomp.to_json, newline]
|
368
|
-
end
|
369
|
-
end.join[indent_n .. -3]
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
def udscript(roles, vars)
|
374
|
-
roles = roles.map{ |role| role.intern }
|
375
|
-
|
376
|
-
unless roles.include? :backend
|
377
|
-
vars = vars.dup
|
378
|
-
vars.delete 'wallet_data'
|
379
|
-
end
|
380
|
-
|
381
|
-
lns = []
|
382
|
-
lns.concat(vars.map do |k, v|
|
383
|
-
var_val = v.include?("\0") ? Base64.encode64(v) : v
|
384
|
-
"export #{k.to_s.upcase}=#{Shellwords.escape(var_val)}\n"
|
385
|
-
end)
|
386
|
-
lns.concat Stacco::Resources::CloudInit::CommonScripts[:before]
|
387
|
-
roles.each{ |role| lns.concat Stacco::Resources::CloudInit::RoleScripts[role] }
|
388
|
-
lns.concat Stacco::Resources::CloudInit::CommonScripts[:after]
|
389
|
-
lns
|
390
|
-
end
|
391
|
-
|
392
|
-
def initialize(stack, template_body)
|
393
|
-
@stack = stack
|
394
|
-
template_body = template_body.read if template_body.respond_to?(:read)
|
395
|
-
@template_body = template_body
|
396
|
-
end
|
397
|
-
|
398
|
-
def result(vars)
|
399
|
-
cloudformation_vars = {}
|
400
|
-
cloudformation_vars.update vars
|
401
|
-
cloudformation_vars.update vars['cloudformation']
|
402
|
-
|
403
|
-
cloudinit_vars = {}
|
404
|
-
cloudinit_vars.update vars
|
405
|
-
cloudinit_vars.update vars['cloud-init']
|
406
|
-
cloudinit_vars.each do |k, v|
|
407
|
-
cloudinit_vars.delete(k) if v.kind_of?(Hash) or v.kind_of?(Array)
|
408
|
-
end
|
409
|
-
|
410
|
-
cloudformation_vars['userdata'] = {}
|
411
|
-
cloudformation_vars['roles'].each do |host_name, host_roles|
|
412
|
-
cloudformation_vars['userdata'][host_name] = udscript(host_roles, cloudinit_vars)
|
413
|
-
end
|
414
|
-
|
415
|
-
b = RenderContext.new(@stack, vars, cloudformation_vars).instance_eval{ binding }
|
416
|
-
ERB.new(@template_body).result(b)
|
417
|
-
end
|
418
|
-
end
|
1
|
+
require 'stacco/resources'
|
2
|
+
require 'stacco/stack'
|
3
|
+
require 'stacco/orchestrator'
|
4
|
+
require 'stacco/template'
|