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/lib/stacco/base.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'aws/with-stacco-patches'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'stacco/base'
|
5
|
+
|
6
|
+
class Stacco::Orchestrator
|
7
|
+
def self.from_config(config_body)
|
8
|
+
config_body = config_body.read if config_body.respond_to?(:read)
|
9
|
+
self.new YAML.load(config_body)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
|
15
|
+
storage_bucket_name = @config['storage_bucket']
|
16
|
+
|
17
|
+
s3 = AWS::S3.new(
|
18
|
+
access_key_id: @config['aws']['access_key_id'],
|
19
|
+
secret_access_key: @config['aws']['secret_access_key'],
|
20
|
+
region: @config['aws']['region']
|
21
|
+
)
|
22
|
+
|
23
|
+
@bucket = s3.buckets[storage_bucket_name]
|
24
|
+
s3.buckets.create(storage_bucket_name) unless @bucket.exists?
|
25
|
+
end
|
26
|
+
|
27
|
+
def stacks
|
28
|
+
@bucket.objects.with_prefix('stack/').to_a[1..-1].map{ |obj| Stacco::Stack.new(obj) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def define_stack(config_body)
|
32
|
+
config_body = config_body.read if config_body.respond_to?(:read)
|
33
|
+
|
34
|
+
config = YAML.load(config_body)
|
35
|
+
|
36
|
+
stack_name = config['name']
|
37
|
+
|
38
|
+
stack_object = @bucket.objects["stack/#{stack_name}"]
|
39
|
+
stack_object.write(config.to_yaml)
|
40
|
+
|
41
|
+
Stacco::Stack.new(stack_object)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'stacco/base'
|
4
|
+
|
5
|
+
module Stacco::Resources
|
6
|
+
RootDir = Pathname.new(File.expand_path("../../../priv", __FILE__))
|
7
|
+
|
8
|
+
templates_dir = Stacco::Resources::RootDir + "templates"
|
9
|
+
Templates = {
|
10
|
+
cloudformation: (templates_dir + "cloudformation.json.erb").read
|
11
|
+
#cloudformation: (templates_dir + "bexng.rb").read
|
12
|
+
}
|
13
|
+
|
14
|
+
module CloudInit
|
15
|
+
cloud_init_scripts_dir = Stacco::Resources::RootDir + "cloud-init"
|
16
|
+
|
17
|
+
CommonScripts = {
|
18
|
+
before: (cloud_init_scripts_dir + "common.before.sh").readlines,
|
19
|
+
after: (cloud_init_scripts_dir + "common.after.sh").readlines
|
20
|
+
}
|
21
|
+
|
22
|
+
RoleScripts = {}
|
23
|
+
(cloud_init_scripts_dir + "roles").children.each{ |path| RoleScripts[path.basename('.sh').to_s.intern] = path.readlines }
|
24
|
+
end
|
25
|
+
end
|
data/lib/stacco/stack.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'aws/with-stacco-patches'
|
6
|
+
|
7
|
+
require 'stacco/base'
|
8
|
+
|
9
|
+
require 'stacco/template/old'
|
10
|
+
|
11
|
+
class Stacco::Stack
|
12
|
+
def initialize(config_object)
|
13
|
+
@config_object = config_object
|
14
|
+
|
15
|
+
aws_creds = self.aws_credentials
|
16
|
+
|
17
|
+
@services = {
|
18
|
+
ec2: AWS::EC2.new(aws_creds),
|
19
|
+
s3: AWS::S3.new(aws_creds),
|
20
|
+
cloudformation: AWS::CloudFormation.new(aws_creds),
|
21
|
+
cloudfront: AWS::CloudFront.new(aws_creds),
|
22
|
+
iam: AWS::IAM.new(aws_creds)
|
23
|
+
}
|
24
|
+
|
25
|
+
@aws_stack = @services[:cloudformation].stacks[self.name]
|
26
|
+
@aws_stack.service_registry = @services
|
27
|
+
end
|
28
|
+
|
29
|
+
def connections
|
30
|
+
connections = {}
|
31
|
+
@aws_stack.instances.each{ |i| connections[i.tags["aws:cloudformation:logical-id"]] = i }
|
32
|
+
connections
|
33
|
+
end
|
34
|
+
|
35
|
+
def resource_summaries
|
36
|
+
@aws_stack.resource_summaries
|
37
|
+
end
|
38
|
+
|
39
|
+
def must_be_up!
|
40
|
+
unless self.up?
|
41
|
+
$stderr.puts "stack #{self.name} is down"
|
42
|
+
Kernel.exit 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def aws_status
|
47
|
+
@aws_stack.status
|
48
|
+
end
|
49
|
+
|
50
|
+
def status
|
51
|
+
self.up? ? self.aws_status : "DOWN"
|
52
|
+
end
|
53
|
+
|
54
|
+
def config
|
55
|
+
YAML.load(@config_object.read)
|
56
|
+
end
|
57
|
+
|
58
|
+
def config=(new_config)
|
59
|
+
@config_object.write(new_config.to_yaml)
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_config
|
63
|
+
# TODO
|
64
|
+
end
|
65
|
+
|
66
|
+
def aws_credentials
|
67
|
+
Hash[ *(self.config['aws'].map{ |k, v| [k.intern, v] }.flatten) ]
|
68
|
+
end
|
69
|
+
|
70
|
+
def description
|
71
|
+
self.config['description']
|
72
|
+
end
|
73
|
+
|
74
|
+
def name
|
75
|
+
self.config['name']
|
76
|
+
end
|
77
|
+
|
78
|
+
def name=(new_name)
|
79
|
+
update_config{ |config| config.merge("name" => new_name) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def up?
|
83
|
+
@aws_stack.exists?
|
84
|
+
end
|
85
|
+
|
86
|
+
def up!
|
87
|
+
if @aws_stack.exists?
|
88
|
+
@aws_stack.update(template: self.cloudformation_template)
|
89
|
+
else
|
90
|
+
@services[:cloudformation].stacks.create(self.name, self.cloudformation_template)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def up_since
|
95
|
+
@aws_stack.creation_time if @aws_stack.exists?
|
96
|
+
end
|
97
|
+
|
98
|
+
def initialize_distributions!
|
99
|
+
@services[:cloudfront].distributions.each do |dist|
|
100
|
+
dist.update do
|
101
|
+
dist.price_class = :"100"
|
102
|
+
dist.certificate = @aws_stack.server_certificates(domain: dist.aliases).first.id
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def down!
|
108
|
+
return false unless self.up?
|
109
|
+
|
110
|
+
@aws_stack.buckets.each{ |bucket| bucket.delete! }
|
111
|
+
@aws_stack.delete
|
112
|
+
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
def cloudformation_template
|
117
|
+
#Kernel.eval(Stacco::Resources::Templates[:cloudformation])
|
118
|
+
tpl = Stacco::Template.const_get(self.config['template']).new
|
119
|
+
tpl.to_json(stack: self)
|
120
|
+
end
|
121
|
+
|
122
|
+
def cloudformation_template_body
|
123
|
+
Stacco::Resources::Templates[:cloudformation]
|
124
|
+
end
|
125
|
+
|
126
|
+
def validate
|
127
|
+
baked_template = self.cloudformation_template
|
128
|
+
test_template = baked_template.gsub(/"[cm][123]\.(\dx)?(small|medium|large)"/, '"m1.small"')
|
129
|
+
|
130
|
+
begin
|
131
|
+
@services[:cloudformation].estimate_template_cost test_template
|
132
|
+
[true]
|
133
|
+
rescue AWS::CloudFormation::Errors::ValidationError => e
|
134
|
+
msg = e.message
|
135
|
+
match = msg.scan(/^Template format error: JSON not well-formed. \(line (\d+), column (\d+)\)$/)
|
136
|
+
if match.length.nonzero?
|
137
|
+
line, column = match.to_a.flatten.map{ |el| el.to_i }
|
138
|
+
[false, msg, [baked_template.split("\n")[line.to_i], column]]
|
139
|
+
else
|
140
|
+
[false, msg]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def iam_private_key
|
146
|
+
@config_object.bucket.objects.with_prefix("sshkey/#{self.name}-").to_a.sort_by{ |obj| obj.key.split('/').last.split('-').last.to_i }.last
|
147
|
+
end
|
148
|
+
|
149
|
+
def iam_keypair_name
|
150
|
+
"stacco-" + self.iam_private_key.key.split('/').last
|
151
|
+
end
|
152
|
+
|
153
|
+
def iam_private_key_material
|
154
|
+
self.iam_private_key.read
|
155
|
+
end
|
156
|
+
|
157
|
+
def stream_events
|
158
|
+
Enumerator.new do |out|
|
159
|
+
known_events = Set.new
|
160
|
+
ticks_without_add = 0
|
161
|
+
|
162
|
+
while self.up?
|
163
|
+
added = 0
|
164
|
+
|
165
|
+
@aws_stack.events.sort_by{ |ev| ev.timestamp }.each do |event|
|
166
|
+
next if known_events.include? event.event_id
|
167
|
+
out.yield event
|
168
|
+
|
169
|
+
known_events.add event.event_id
|
170
|
+
added += 1
|
171
|
+
ticks_without_add = 0
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
ticks_without_add += 1 if added == 0
|
176
|
+
|
177
|
+
if ticks_without_add >= 8 and (Math.log2(ticks_without_add) % 1) == 0.0
|
178
|
+
jobs = @aws_stack.resource_summaries
|
179
|
+
active_jobs = jobs.find_all{ |job| job[:resource_status] =~ /IN_PROGRESS$/ }.map{ |job| job[:logical_resource_id] }.sort
|
180
|
+
unless active_jobs.empty?
|
181
|
+
out.yield OpenStruct.new(
|
182
|
+
logical_resource_id: "Scheduler",
|
183
|
+
status: "WAIT",
|
184
|
+
operation: "WAIT",
|
185
|
+
timestamp: Time.now,
|
186
|
+
error: "waiting on #{active_jobs.join(', ')}"
|
187
|
+
)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
Kernel.sleep 2
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'base64'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'json'
|
7
|
+
require 'erb'
|
8
|
+
require 'open-uri'
|
9
|
+
|
10
|
+
require 'stacco/base'
|
11
|
+
|
12
|
+
class String
|
13
|
+
UpcasedInitialisms = Set['ec2', 'iam', 'elb', 'vpc', 'dhcp', 'aws', 'dns']
|
14
|
+
|
15
|
+
def camelize
|
16
|
+
return self if (self != self.downcase)
|
17
|
+
self.split('_').map{ |part| UpcasedInitialisms.include?(part) ? part.upcase : part.capitalize }.join
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Symbol
|
22
|
+
def camelize
|
23
|
+
self.to_s.camelize.intern
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Stacco::Template
|
28
|
+
def initialize
|
29
|
+
@parameters = []
|
30
|
+
@resources = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def udscript(roles, vars)
|
34
|
+
roles = roles.map{ |role| role.intern }
|
35
|
+
|
36
|
+
unless roles.include? :backend
|
37
|
+
vars = vars.dup
|
38
|
+
vars.delete 'wallet_data'
|
39
|
+
end
|
40
|
+
|
41
|
+
lns = []
|
42
|
+
lns.concat(vars.map do |k, v|
|
43
|
+
var_val = v.include?("\0") ? Base64.encode64(v) : v
|
44
|
+
"export #{k.to_s.upcase}=#{Shellwords.escape(var_val)}\n"
|
45
|
+
end)
|
46
|
+
lns.concat Stacco::Resources::CloudInit::CommonScripts[:before]
|
47
|
+
roles.each{ |role| lns.concat Stacco::Resources::CloudInit::RoleScripts[role] }
|
48
|
+
lns.concat Stacco::Resources::CloudInit::CommonScripts[:after]
|
49
|
+
lns
|
50
|
+
end
|
51
|
+
|
52
|
+
class Path < Array
|
53
|
+
def elements_for_json
|
54
|
+
self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class ColonPath < Path
|
59
|
+
def to_s
|
60
|
+
self.elements_for_json.map{ |sym| sym.to_s.camelize }.join('::')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class ConcatenatedPath < Path
|
65
|
+
def to_s
|
66
|
+
self.elements_for_json.map{ |sym| sym.to_s.camelize }.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Var < ConcatenatedPath
|
71
|
+
def elements_for_json
|
72
|
+
[:stacco] + self
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class StackVar < Var
|
77
|
+
def elements_for_json
|
78
|
+
[:stacco, :stack] + self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Type < ColonPath
|
83
|
+
def elements_for_json
|
84
|
+
[:aws] + self
|
85
|
+
end
|
86
|
+
|
87
|
+
def resource(name = nil)
|
88
|
+
Resource[*(self + [name].compact)]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class Resource < ConcatenatedPath
|
93
|
+
def elements_for_json
|
94
|
+
[:stacco, :resource] + self
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Mapping < ConcatenatedPath
|
99
|
+
def elements_for_json
|
100
|
+
[:stacco, :mapping] + self
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
AttachmentTypes = {
|
105
|
+
Set[Type[:ec2, :vpc], Type[:ec2, :internet_gateway]] => Type[:ec2, :vpc_gateway_attachment],
|
106
|
+
}
|
107
|
+
|
108
|
+
|
109
|
+
def parameter(name, type, opts = {})
|
110
|
+
@parameters.push [name, type, opts]
|
111
|
+
end
|
112
|
+
|
113
|
+
def resource(type, opts = {})
|
114
|
+
name = type.resource(opts.delete(:name))
|
115
|
+
@resources.push [name, type, opts]
|
116
|
+
name
|
117
|
+
end
|
118
|
+
|
119
|
+
def mapping(name, hsh)
|
120
|
+
@mappings.push [name, hsh]
|
121
|
+
end
|
122
|
+
|
123
|
+
def attach(resource_a, resource_b, opts = {})
|
124
|
+
end
|
125
|
+
|
126
|
+
def render
|
127
|
+
tpl = {AWSTemplateFormatVersion: "2010-09-09"}
|
128
|
+
tpl[:description] = "Stacco stack"
|
129
|
+
|
130
|
+
tpl[:Mappings] = {}
|
131
|
+
|
132
|
+
@mappings.each do |(name, hsh)|
|
133
|
+
name = [name] if name.kind_of?(String) or name.kind_of?(Symbol)
|
134
|
+
tpl[:Mappings][Mapping[*name]] = hsh
|
135
|
+
end
|
136
|
+
|
137
|
+
tpl[:Parameters] = {}
|
138
|
+
|
139
|
+
@parameters.each do |(name, type, args)|
|
140
|
+
name = [name] if name.kind_of?(String) or name.kind_of?(Symbol)
|
141
|
+
constraints = {}
|
142
|
+
args.merge(type: type.camelize).each do |k,v|
|
143
|
+
constraints[k.camelize] = v
|
144
|
+
end
|
145
|
+
|
146
|
+
tpl[:Parameters][name] = constraints
|
147
|
+
end
|
148
|
+
|
149
|
+
tpl[:Resources] = {}
|
150
|
+
|
151
|
+
@resources.each do |(name, type, args)|
|
152
|
+
name = [name] if name.kind_of?(String) or name.kind_of?(Symbol)
|
153
|
+
properties = {}
|
154
|
+
args.each do |k,v|
|
155
|
+
properties[k.camelize] = v
|
156
|
+
end
|
157
|
+
|
158
|
+
tpl[:Resources][name] = {"Type" => type, "DependsOn" => [], "Properties" => properties}
|
159
|
+
end
|
160
|
+
|
161
|
+
tpl
|
162
|
+
end
|
163
|
+
|
164
|
+
def export_for_json(o, is_key = false)
|
165
|
+
case o
|
166
|
+
when Numeric
|
167
|
+
o.to_s
|
168
|
+
when TrueClass, FalseClass
|
169
|
+
o.to_s
|
170
|
+
when NilClass
|
171
|
+
raise ArgumentError, o
|
172
|
+
when Var
|
173
|
+
if is_key
|
174
|
+
o.to_s
|
175
|
+
else
|
176
|
+
{"Ref" => o.to_s}
|
177
|
+
end
|
178
|
+
when Symbol
|
179
|
+
ConcatenatedPath[o].to_s
|
180
|
+
when Path
|
181
|
+
o.to_s
|
182
|
+
when Array
|
183
|
+
o.map{ |el| export_for_json(el) }
|
184
|
+
when Hash
|
185
|
+
nh = {}
|
186
|
+
o.each{ |k, v| nh[ export_for_json(k, true) ] = export_for_json(v) }
|
187
|
+
nh
|
188
|
+
when Regexp
|
189
|
+
o.source
|
190
|
+
else
|
191
|
+
o
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_json(opts = {})
|
196
|
+
self.export_for_json(self.render).to_json
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
if __FILE__ == $0
|
201
|
+
tpl = Stacco::Template.new
|
202
|
+
|
203
|
+
if ARGV.delete('-d')
|
204
|
+
require 'pp'
|
205
|
+
pp JSON.parse(tpl.to_json)
|
206
|
+
else
|
207
|
+
puts tpl.to_json
|
208
|
+
end
|
209
|
+
end
|