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.
@@ -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