stacco 0.1.10 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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'
|