kumogata 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,324 @@
1
+ class Kumogata::Client
2
+ def initialize(options)
3
+ @options = options
4
+ @options = Hashie::Mash.new(@options) unless @options.kind_of?(Hashie::Mash)
5
+ @cloud_formation = AWS::CloudFormation.new
6
+ end
7
+
8
+ def create(path_or_url, stack_name = nil)
9
+ @options.delete_stack = false if stack_name
10
+ template = open_template(path_or_url)
11
+
12
+ if @options.delete_stack?
13
+ template['Resources'].each do |k, v|
14
+ v['DeletionPolicy'] = 'Retain'
15
+ end
16
+ end
17
+
18
+ create_stack(template, stack_name)
19
+ nil
20
+ end
21
+
22
+ def validate(path_or_url)
23
+ template = open_template(path_or_url)
24
+ validate_template(template)
25
+ nil
26
+ end
27
+
28
+ def convert(path_or_url)
29
+ template = open_template(path_or_url)
30
+
31
+ if ruby_template?(path_or_url)
32
+ JSON.pretty_generate(template)
33
+ else
34
+ devaluate_template(template).chomp
35
+ end
36
+ end
37
+
38
+ def update(path_or_url, stack_name)
39
+ template = open(path_or_url) do |f|
40
+ evaluate_template(f)
41
+ end
42
+
43
+ update_stack(template, stack_name)
44
+ nil
45
+ end
46
+
47
+ def delete(stack_name)
48
+ if @options.force? or agree("Aare you sure you want to delete `#{stack_name}`? ".yellow)
49
+ delete_stack(stack_name)
50
+ end
51
+
52
+ nil
53
+ end
54
+
55
+ def list(stack_name = nil)
56
+ stacks = describe_stacks(stack_name)
57
+ JSON.pretty_generate(stacks)
58
+ end
59
+
60
+ private ###########################################################
61
+
62
+ def open_template(path_or_url)
63
+ open(path_or_url) do |f|
64
+ if ruby_template?(path_or_url)
65
+ evaluate_template(f)
66
+ else
67
+ JSON.parse(f.read)
68
+ end
69
+ end
70
+ end
71
+
72
+ def ruby_template?(path_or_url)
73
+ File.extname(path_or_url) == '.rb'
74
+ end
75
+
76
+ def evaluate_template(template)
77
+ key_converter = proc do |key|
78
+ key = key.to_s
79
+ key.gsub!('__', '::') if @options.replace_underscore?
80
+ key
81
+ end
82
+
83
+ value_converter = proc do |v|
84
+ case v
85
+ when Hash, Array
86
+ v
87
+ else
88
+ v.to_s
89
+ end
90
+ end
91
+
92
+ Dslh.eval(template.read, {
93
+ :key_conv => key_converter,
94
+ :value_conv => value_converter,
95
+ :scope_hook => method(:define_template_func),
96
+ :filename => template.path,
97
+ })
98
+ end
99
+
100
+ def devaluate_template(template)
101
+ exclude_key = proc do |k|
102
+ k = k.to_s.gsub('::', '__')
103
+ k !~ /\A[_a-z]\w+\Z/i and k !~ %r|\A/\S*\Z|
104
+ end
105
+
106
+ key_conv = proc do |k|
107
+ k = k.to_s
108
+
109
+ if k =~ %r|\A/\S*\Z|
110
+ proc do |v, nested|
111
+ if nested
112
+ "_path(#{k.inspect}) #{v}"
113
+ else
114
+ "_path #{k.inspect}, #{v}"
115
+ end
116
+ end
117
+ else
118
+ k.gsub('::', '__')
119
+ end
120
+ end
121
+
122
+ Dslh.deval(template, :key_conv => key_conv, :exclude_key => exclude_key)
123
+ end
124
+
125
+ def define_template_func(scope)
126
+ scope.instance_eval do
127
+ def _user_data(data)
128
+ data.strip_lines.encode64
129
+ end
130
+
131
+ def _path(path, value = nil, &block)
132
+ if block
133
+ value = Dslh::ScopeBlock.nest(binding, 'block')
134
+ end
135
+
136
+ @__hash__[path] = value
137
+ end
138
+ end
139
+ end
140
+
141
+ def describe_stacks(stack_name)
142
+ AWS.memoize do
143
+ stacks = @cloud_formation.stacks
144
+ stacks = stacks.select {|i| i.name == stack_name } if stack_name
145
+
146
+ stacks.map do |stack|
147
+ {
148
+ 'StackName' => stack.name,
149
+ 'CreationTime' => stack.creation_time,
150
+ 'StackStatus' => stack.status,
151
+ 'Description' => stack.description,
152
+ }
153
+ end
154
+ end
155
+ end
156
+
157
+ def create_stack(template, stack_name)
158
+ stack_name = stack_name || 'kumogata-' + UUIDTools::UUID.timestamp_create
159
+
160
+ Kumogata.logger.info("Creating stack: #{stack_name}".cyan)
161
+ stack = @cloud_formation.stacks.create(stack_name, template.to_json, build_create_options)
162
+
163
+ unless while_in_progress(stack, 'CREATE_COMPLETE')
164
+ errmsgs = ['Create failed']
165
+ errmsgs << stack_name
166
+ errmsgs << sstack.tatus_reason if stack.status_reason
167
+ raise errmsgs.join(': ')
168
+ end
169
+
170
+ outputs = outputs_for(stack)
171
+ summaries = resource_summaries_for(stack)
172
+
173
+ if @options.delete_stack?
174
+ delete_stack(stack_name)
175
+ end
176
+
177
+ output_result(stack_name, outputs, summaries)
178
+ end
179
+
180
+ def update_stack(template, stack_name)
181
+ stack = @cloud_formation.stacks[stack_name]
182
+ stack.status
183
+ stack.update(build_update_options(template.to_json))
184
+
185
+ Kumogata.logger.info("Updating stack: #{stack_name}".green)
186
+
187
+ unless while_in_progress(stack, 'UPDATE_COMPLETE')
188
+ errmsgs = ['Update failed']
189
+ errmsgs << stack_name
190
+ errmsgs << sstack.tatus_reason if stack.status_reason
191
+ raise errmsgs.join(': ')
192
+ end
193
+
194
+ outputs = outputs_for(stack)
195
+ summaries = resource_summaries_for(stack)
196
+ output_result(stack_name, outputs, summaries)
197
+ end
198
+
199
+ def delete_stack(stack_name)
200
+ stack = @cloud_formation.stacks[stack_name]
201
+ stack.status
202
+
203
+ Kumogata.logger.info("Deleting stack: #{stack_name}".red)
204
+ stack.delete
205
+
206
+ completed = false
207
+
208
+ begin
209
+ completed = while_in_progress(stack, 'DELETE_COMPLETE')
210
+ rescue AWS::CloudFormation::Errors::ValidationError
211
+ # Handle `Stack does not exist`
212
+ completed = true
213
+ Kumogata.logger.info('Successfully')
214
+ end
215
+
216
+ unless completed
217
+ errmsgs = ['Delete failed']
218
+ errmsgs << stack_name
219
+ errmsgs << sstack.tatus_reason if stack.status_reason
220
+ raise errmsgs.join(': ')
221
+ end
222
+ end
223
+
224
+ def while_in_progress(stack, complete_status)
225
+ while stack.status =~ /_IN_PROGRESS\Z/
226
+ print '.'.intense_black unless @options.debug?
227
+ sleep 1
228
+ end
229
+
230
+ completed = (stack.status == complete_status)
231
+ Kumogata.logger.info(completed ? 'Successfully' : 'Failed')
232
+ return completed
233
+ end
234
+
235
+ def build_create_options
236
+ opts = {}
237
+ add_parameters(opts)
238
+
239
+ [:capabilities, :disable_rollback, :notify, :timeout].each do |k|
240
+ opts[k] = @options[k] if @options[k]
241
+ end
242
+
243
+ return opts
244
+ end
245
+
246
+ def build_update_options(template)
247
+ opts = {:template => template}
248
+ add_parameters(opts)
249
+ return opts
250
+ end
251
+
252
+ def add_parameters(hash)
253
+ if @options.parameters?
254
+ parameters = {}
255
+
256
+ @options.parameters.each do |i|
257
+ key, value = i.split('=', 2)
258
+ parameters[key] = value
259
+ end
260
+
261
+ hash[:parameters] = parameters
262
+ end
263
+ end
264
+
265
+ def validate_template(template)
266
+ result = @cloud_formation.validate_template(template.to_json)
267
+
268
+ if result[:code]
269
+ raise result.values_at(:code, :message).join(': ')
270
+ end
271
+
272
+ Kumogata.logger.info('Template validated successfully'.green)
273
+ end
274
+
275
+ def outputs_for(stack)
276
+ outputs_hash = {}
277
+
278
+ stack.outputs.each do |output|
279
+ outputs_hash[output.key] = output.value
280
+ end
281
+
282
+ return outputs_hash
283
+ end
284
+
285
+ def resource_summaries_for(stack)
286
+ stack.resource_summaries.map do |summary|
287
+ summary_hash = {}
288
+
289
+ [
290
+ :logical_resource_id,
291
+ :physical_resource_id,
292
+ :resource_type,
293
+ :resource_status,
294
+ :resource_status_reason,
295
+ :last_updated_timestamp
296
+ ].each do |k|
297
+ summary_hash[k.to_s.camelcase] = summary[k]
298
+ end
299
+
300
+ summary_hash
301
+ end
302
+ end
303
+
304
+ def output_result(stack_name, outputs, summaries)
305
+ puts <<-EOS
306
+
307
+ Outputs:
308
+ #{JSON.pretty_generate(outputs)}
309
+
310
+ Stack Resource Summaries:
311
+ #{JSON.pretty_generate(summaries)}
312
+
313
+ (Save to `#{@options.result_log}`)
314
+ EOS
315
+
316
+ open(@options.result_log, 'wb') do |f|
317
+ f.puts JSON.pretty_generate({
318
+ 'StackName' => stack_name,
319
+ 'Outputs' => outputs,
320
+ 'StackResourceSummaries' => summaries,
321
+ })
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,41 @@
1
+ require 'term/ansicolor'
2
+
3
+ class String
4
+ @@colorize = false
5
+
6
+ class << self
7
+ def colorize=(value)
8
+ @@colorize = value
9
+ end
10
+
11
+ def colorize
12
+ @@colorize
13
+ end
14
+ end # of class methods
15
+
16
+ Term::ANSIColor::Attribute.named_attributes.map do |attribute|
17
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
18
+ def #{attribute.name}
19
+ if @@colorize
20
+ Term::ANSIColor.send(#{attribute.name.inspect}, self)
21
+ else
22
+ self
23
+ end
24
+ end
25
+ EOS
26
+ end
27
+
28
+ def camelcase
29
+ self.split(/[-_]/).map {|str|
30
+ str[0, 1].upcase + str[1..-1].downcase
31
+ }.join
32
+ end
33
+
34
+ def encode64
35
+ Base64.encode64(self).delete("\n")
36
+ end
37
+
38
+ def strip_lines
39
+ self.strip.split("\n").map {|i| i.strip }.join("\n")
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ module Kumogata
2
+ def self.logger
3
+ Kumogata::Logger.instance
4
+ end
5
+
6
+ class Logger < ::Logger
7
+ include Singleton
8
+
9
+ def initialize
10
+ super($stdout)
11
+
12
+ self.formatter = proc do |severity, datetime, progname, msg|
13
+ "#{msg}\n"
14
+ end
15
+
16
+ self.level = Logger::INFO
17
+ end
18
+
19
+ def set_debug(value)
20
+ self.level = value ? Logger::DEBUG : Logger::INFO
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Kumogata
2
+ VERSION = '0.1.0'
3
+ end
data/lib/kumogata.rb ADDED
@@ -0,0 +1,19 @@
1
+ module Kumogata; end
2
+ require 'kumogata/version'
3
+
4
+ require 'aws-sdk'
5
+ require 'base64'
6
+ require 'dslh'
7
+ require 'hashie'
8
+ require 'highline/import'
9
+ require 'json'
10
+ require 'logger'
11
+ require 'open-uri'
12
+ require 'optparse'
13
+ require 'singleton'
14
+ require 'uuidtools'
15
+
16
+ require 'kumogata/argument_parser'
17
+ require 'kumogata/client'
18
+ require 'kumogata/ext/string_ext'
19
+ require 'kumogata/logger'
@@ -0,0 +1,26 @@
1
+ {
2
+ "variables": {
3
+ "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
4
+ "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}"
5
+ },
6
+ "builders": [{
7
+ "type": "amazon-ebs",
8
+ "ami_name": "CentOS-6.4-x86_64-with_Updates-{{timestamp}}",
9
+ "access_key": "{{user `aws_access_key`}}",
10
+ "secret_key": "{{user `aws_secret_key`}}",
11
+ "region": "ap-northeast-1",
12
+ "source_ami": "ami-31e86030",
13
+ "instance_type": "t1.micro",
14
+ "ssh_username": "root",
15
+ "ssh_timeout": "3m"
16
+ }],
17
+ "provisioners": [{
18
+ "type": "shell",
19
+ "inline": [
20
+ "yum install -y http://ftp.iij.ad.jp/pub/linux/fedora/epel/6/i386/epel-release-6-8.noarch.rpm",
21
+ "yum install -y cloud-init",
22
+ "yum install -y https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.amzn1.noarch.rpm",
23
+ "rm -rf /root/.ssh"
24
+ ]
25
+ }]
26
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "variables": {
3
+ "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
4
+ "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}"
5
+ },
6
+ "builders": [{
7
+ "type": "amazon-ebs",
8
+ "ami_name": "Ubuntu-12.04.4-LTS-x86_64-ebs-{{timestamp}}",
9
+ "access_key": "{{user `aws_access_key`}}",
10
+ "secret_key": "{{user `aws_secret_key`}}",
11
+ "region": "ap-northeast-1",
12
+ "source_ami": "ami-f381f5f2",
13
+ "instance_type": "t1.micro",
14
+ "ssh_username": "ubuntu",
15
+ "ssh_timeout": "3m"
16
+ }],
17
+ "provisioners": [{
18
+ "type": "shell",
19
+ "inline": [
20
+ "sudo apt-get -y install python-setuptools",
21
+ "wget -P /tmp https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz",
22
+ "sudo mkdir -p /tmp/aws-cfn-bootstrap-latest",
23
+ "sudo tar xvfz /tmp/aws-cfn-bootstrap-latest.tar.gz --strip-components=1 -C /tmp/aws-cfn-bootstrap-latest",
24
+ "sudo easy_install /tmp/aws-cfn-bootstrap-latest/",
25
+ "sudo rm -rf /tmp/aws-cfn-bootstrap-latest"
26
+ ]
27
+ }]
28
+ }
@@ -0,0 +1,96 @@
1
+ describe 'Kumogata::Client#convert' do
2
+ it 'convert Ruby template to JSON template' do
3
+ template = <<-EOS
4
+ Resources do
5
+ myEC2Instance do
6
+ Type "AWS::EC2::Instance"
7
+ Properties do
8
+ ImageId "ami-XXXXXXXX"
9
+ InstanceType "t1.micro"
10
+ end
11
+ end
12
+ end
13
+
14
+ Outputs do
15
+ AZ do
16
+ Value do
17
+ Fn__GetAtt "myEC2Instance", "AvailabilityZone"
18
+ end
19
+ end
20
+ end
21
+ EOS
22
+
23
+ json_template = run_client(:convert, :template => template)
24
+
25
+ expect(json_template).to eq((<<-EOS).chomp)
26
+ {
27
+ "Resources": {
28
+ "myEC2Instance": {
29
+ "Type": "AWS::EC2::Instance",
30
+ "Properties": {
31
+ "ImageId": "ami-XXXXXXXX",
32
+ "InstanceType": "t1.micro"
33
+ }
34
+ }
35
+ },
36
+ "Outputs": {
37
+ "AZ": {
38
+ "Value": {
39
+ "Fn::GetAtt": [
40
+ "myEC2Instance",
41
+ "AvailabilityZone"
42
+ ]
43
+ }
44
+ }
45
+ }
46
+ }
47
+ EOS
48
+ end
49
+
50
+ it 'convert Ruby template to JSON template' do
51
+ template = <<-EOS
52
+ {
53
+ "Resources": {
54
+ "myEC2Instance": {
55
+ "Type": "AWS::EC2::Instance",
56
+ "Properties": {
57
+ "ImageId": "ami-07f68106",
58
+ "InstanceType": "t1.micro"
59
+ }
60
+ }
61
+ },
62
+ "Outputs": {
63
+ "AZ": {
64
+ "Value": {
65
+ "Fn::GetAtt": [
66
+ "myEC2Instance",
67
+ "AvailabilityZone"
68
+ ]
69
+ }
70
+ }
71
+ }
72
+ }
73
+ EOS
74
+
75
+ ruby_template = run_client(:convert, :template => template, :template_ext => '.template')
76
+
77
+ expect(ruby_template).to eq((<<-EOS).chomp)
78
+ Resources do
79
+ myEC2Instance do
80
+ Type "AWS::EC2::Instance"
81
+ Properties do
82
+ ImageId "ami-07f68106"
83
+ InstanceType "t1.micro"
84
+ end
85
+ end
86
+ end
87
+ Outputs do
88
+ AZ do
89
+ Value do
90
+ Fn__GetAtt "myEC2Instance", "AvailabilityZone"
91
+ end
92
+ end
93
+ end
94
+ EOS
95
+ end
96
+ end