kumogata 0.1.0

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,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