stacco 0.1.10 → 0.1.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ module Stacco
2
+ end
@@ -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
@@ -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