cloudformation-tool 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +339 -0
- data/README.md +204 -0
- data/bin/cftool +9 -0
- data/lib/cloud_formation_tool.rb +94 -0
- data/lib/cloud_formation_tool/cli.rb +5 -0
- data/lib/cloud_formation_tool/cli/compile.rb +19 -0
- data/lib/cloud_formation_tool/cli/create.rb +50 -0
- data/lib/cloud_formation_tool/cli/delete.rb +18 -0
- data/lib/cloud_formation_tool/cli/list_stacks.rb +15 -0
- data/lib/cloud_formation_tool/cli/main.rb +31 -0
- data/lib/cloud_formation_tool/cli/monitor.rb +28 -0
- data/lib/cloud_formation_tool/cli/parameters.rb +20 -0
- data/lib/cloud_formation_tool/cli/servers.rb +30 -0
- data/lib/cloud_formation_tool/cli/status.rb +18 -0
- data/lib/cloud_formation_tool/cloud_formation.rb +153 -0
- data/lib/cloud_formation_tool/cloud_formation/lambda_code.rb +41 -0
- data/lib/cloud_formation_tool/cloud_formation/stack.rb +152 -0
- data/lib/cloud_formation_tool/cloud_init.rb +86 -0
- data/lib/cloud_formation_tool/errors.rb +11 -0
- data/lib/cloud_formation_tool/storable.rb +41 -0
- data/lib/cloud_formation_tool/version.rb +3 -0
- metadata +109 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module CloudFormationTool
|
4
|
+
class CloudFormation
|
5
|
+
|
6
|
+
class LambdaCode
|
7
|
+
include Storable
|
8
|
+
|
9
|
+
def initialize(url)
|
10
|
+
log "Downloading Lambda code from #{url}"
|
11
|
+
res = fetch(url)
|
12
|
+
|
13
|
+
@s3_url = URI(upload(make_filename(url.split('.').last), res.body, res['content-type']))
|
14
|
+
log "uploaded Lambda function to #{@s3_url}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(uri_str, limit = 10)
|
18
|
+
raise ArgumentError, 'too many HTTP redirects' if limit == 0
|
19
|
+
response = Net::HTTP.get_response(URI(uri_str))
|
20
|
+
case response
|
21
|
+
when Net::HTTPSuccess then
|
22
|
+
response
|
23
|
+
when Net::HTTPRedirection then
|
24
|
+
location = response['location']
|
25
|
+
log "redirected to #{location}"
|
26
|
+
fetch(location, limit - 1)
|
27
|
+
else
|
28
|
+
raise AppError, "Error downloading #{url}: #{response.value}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_cloudformation
|
33
|
+
{
|
34
|
+
'S3Bucket' => @s3_url.hostname.split('.').first,
|
35
|
+
'S3Key' => @s3_url.path[1..-1]
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module CloudFormationTool
|
2
|
+
class CloudFormation
|
3
|
+
|
4
|
+
class Stack
|
5
|
+
include Enumerable
|
6
|
+
include Storable
|
7
|
+
include CloudFormationTool
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@seenev = Set.new
|
14
|
+
@watch_timeouts = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete
|
18
|
+
awscf.delete_stack stack_name: @name
|
19
|
+
end
|
20
|
+
|
21
|
+
def exist?
|
22
|
+
begin
|
23
|
+
awscf.describe_stacks stack_name: name
|
24
|
+
true
|
25
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(url, filepath, params = {})
|
31
|
+
log "Updating existing stack '#{name}' from '#{filepath}' params #{params.inspect}"
|
32
|
+
resp = awscf.update_stack({
|
33
|
+
stack_name: @name,
|
34
|
+
template_url: url,
|
35
|
+
capabilities: %w(CAPABILITY_IAM CAPABILITY_NAMED_IAM),
|
36
|
+
parameters: params.collect do |k,v|
|
37
|
+
{
|
38
|
+
parameter_key: k.to_s,
|
39
|
+
parameter_value: v.to_s,
|
40
|
+
use_previous_value: false,
|
41
|
+
}
|
42
|
+
end
|
43
|
+
})
|
44
|
+
resp.stack_id
|
45
|
+
end
|
46
|
+
|
47
|
+
def create(template, params = {})
|
48
|
+
tmpl = CloudFormation.parse(template).to_yaml
|
49
|
+
url = upload(make_filename('yaml'), tmpl)
|
50
|
+
return update(url, template, params) if exist?
|
51
|
+
log "Creating stack '#{name}' from '#{template}' params #{params.inspect}"
|
52
|
+
resp = awscf.create_stack({
|
53
|
+
stack_name: @name,
|
54
|
+
template_url: url,
|
55
|
+
capabilities: %w(CAPABILITY_IAM CAPABILITY_NAMED_IAM),
|
56
|
+
on_failure: "DO_NOTHING", ##"ROLLBACK",
|
57
|
+
parameters: params.collect do |k,v|
|
58
|
+
{
|
59
|
+
parameter_key: k.to_s,
|
60
|
+
parameter_value: v.to_s,
|
61
|
+
use_previous_value: false,
|
62
|
+
}
|
63
|
+
end
|
64
|
+
})
|
65
|
+
resp.stack_id
|
66
|
+
end
|
67
|
+
|
68
|
+
def resources
|
69
|
+
begin
|
70
|
+
resp = awscf.describe_stack_resources stack_name: @name
|
71
|
+
resp.stack_resources
|
72
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
73
|
+
raise Errors::AppError, "Failed to get resources: #{e.message}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def asgroups
|
78
|
+
resources.select do |res|
|
79
|
+
res.resource_type == 'AWS::AutoScaling::AutoScalingGroup'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def see_events
|
84
|
+
each { |e| @seenev << e.event_id }
|
85
|
+
end
|
86
|
+
|
87
|
+
def monitor(start_time = nil)
|
88
|
+
done = false
|
89
|
+
begin
|
90
|
+
until done
|
91
|
+
reverse_each do |ev|
|
92
|
+
next if @seenev.add?(ev.event_id).nil?
|
93
|
+
text = "#{ev.timestamp.strftime "%Y-%m-%d %H:%M:%S"}| " + %w(
|
94
|
+
resource_type:40
|
95
|
+
logical_resource_id:38
|
96
|
+
resource_status
|
97
|
+
).collect { |field|
|
98
|
+
(name,size) = field.split(":")
|
99
|
+
size ||= 1
|
100
|
+
ev.send(name.to_sym).ljust(size.to_i, ' ')
|
101
|
+
}.join(" ")
|
102
|
+
text += " " + ev.resource_status_reason if ev.resource_status =~ /_FAILED/
|
103
|
+
if start_time.nil? or start_time < ev.timestamp
|
104
|
+
puts text
|
105
|
+
end
|
106
|
+
done = (ev.resource_type == "AWS::CloudFormation::Stack" and ev.resource_status =~ /(_COMPLETE|_FAILED)$/)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
rescue CloudFormationTool::Errors::StackDoesNotExistError => e
|
110
|
+
puts "Stack #{name} does not exist"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def each
|
115
|
+
token = nil
|
116
|
+
sleep(if @_last_poll_time.nil?
|
117
|
+
0
|
118
|
+
else
|
119
|
+
diff = Time.now - @_last_poll_time
|
120
|
+
if diff < 1
|
121
|
+
diff
|
122
|
+
else
|
123
|
+
0
|
124
|
+
end
|
125
|
+
end)
|
126
|
+
begin
|
127
|
+
resp = awscf.describe_stack_events stack_name: name, next_token: token
|
128
|
+
@watch_timeouts = 0
|
129
|
+
resp.stack_events.each do |ev|
|
130
|
+
yield ev
|
131
|
+
end
|
132
|
+
rescue Aws::CloudFormation::Errors::Throttling => e
|
133
|
+
sleep 1
|
134
|
+
retry
|
135
|
+
rescue Seahorse::Client::NetworkingError => e # we get this when there's a timeout
|
136
|
+
if (@watch_timeouts += 1) > 5
|
137
|
+
raise AppError, "Too many timeouts!"
|
138
|
+
else
|
139
|
+
retry
|
140
|
+
end
|
141
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
142
|
+
if e.message =~ /does not exist/
|
143
|
+
raise CloudFormationTool::Errors::StackDoesNotExistError, "Stack does not exist"
|
144
|
+
else
|
145
|
+
raise e
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
module CloudFormationTool
|
4
|
+
class CloudInit
|
5
|
+
include Storable
|
6
|
+
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
log "Loading #{path}"
|
12
|
+
begin
|
13
|
+
@initfile = YAML.load(File.read(path)).to_h
|
14
|
+
rescue Errno::ENOENT => e
|
15
|
+
raise AppError.new("Error reading #{@path}: " + e.message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def compile
|
20
|
+
basepath = File.dirname(path)
|
21
|
+
basepath = "." if basepath.empty?
|
22
|
+
@initfile['write_files'] = (@initfile['write_files'] || []).collect do |file|
|
23
|
+
if file['file']
|
24
|
+
begin
|
25
|
+
read_file_content(basepath + "/" + file.delete('file'), file)
|
26
|
+
rescue Errno::EISDIR => e
|
27
|
+
raise AppError, "#{path} - error loading embedded file for #{file['path']}: " + e.message
|
28
|
+
rescue Errno::ENOENT => e
|
29
|
+
raise AppError, "#{path} - error loading embedded file for #{file['path']}: " + e.message
|
30
|
+
end
|
31
|
+
else
|
32
|
+
file
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@initfile['write_files'] += (@initfile.delete('write_directories') || []).collect do |directory|
|
36
|
+
realdir = "#{basepath}/#{directory['source']}"
|
37
|
+
raise AppError.new("Cloud-init file #{path} references missing directory #{realdir}") unless File.exist? realdir
|
38
|
+
read_dir_files(realdir, directory['target'])
|
39
|
+
end.flatten
|
40
|
+
"#cloud-config\n" + @initfile.to_yaml
|
41
|
+
end
|
42
|
+
|
43
|
+
def encode
|
44
|
+
yamlout = compile
|
45
|
+
if yamlout.size > 16384 # max AWS EC2 user data size - try compressing it
|
46
|
+
yamlout = Zlib::Deflate.new(nil, 31).deflate(yamlout, Zlib::FINISH) # 31 is the magic word to have deflate create a gzip compatible header
|
47
|
+
end
|
48
|
+
if yamlout.size > 16384 # still to big, we should upload to S3 and create an include file
|
49
|
+
url = upload make_filename('init'),
|
50
|
+
yamlout, 'text/cloud-config'
|
51
|
+
log "Wrote cloud config to #{url}"
|
52
|
+
[ "#include", url ].join "\n"
|
53
|
+
else
|
54
|
+
yamlout
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_file_content(filepath, spec)
|
59
|
+
spec['encoding'] = 'base64'
|
60
|
+
spec['content'] = Base64.strict_encode64(File.read(filepath))
|
61
|
+
spec
|
62
|
+
end
|
63
|
+
|
64
|
+
def read_dir_files(source, target)
|
65
|
+
Dir.entries(source).select do |entry|
|
66
|
+
entry !~ /^\.{1,2}$/
|
67
|
+
end.collect do |entry|
|
68
|
+
path = source + "/" + entry
|
69
|
+
targetpath = target + "/" + entry
|
70
|
+
if File.directory? path
|
71
|
+
read_dir_files(path, targetpath)
|
72
|
+
else
|
73
|
+
[ read_file_content(path, {
|
74
|
+
'path' => targetpath,
|
75
|
+
'permissions' => (("%o" % File.stat(path).mode)[-4..-1])
|
76
|
+
}) ]
|
77
|
+
end
|
78
|
+
end.flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_base64
|
82
|
+
Base64.encode64(encode)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module CloudFormationTool
|
4
|
+
module Storable
|
5
|
+
include CloudFormationTool
|
6
|
+
|
7
|
+
def make_filename(ext = '')
|
8
|
+
base = "#{File.basename(Dir.pwd)}-#{Time.now.strftime("%Y%m%d%H%M%S")}"
|
9
|
+
if ext.empty?
|
10
|
+
base
|
11
|
+
else
|
12
|
+
"#{base}.#{ext}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def upload(path, content, mime_type = 'text/yaml')
|
17
|
+
md5 = Digest::MD5.hexdigest content
|
18
|
+
prefix = "#{md5[0]}/#{md5[1..2]}/#{md5}"
|
19
|
+
b = Aws::S3::Bucket.new(s3_bucket_name(region), client: awss3(region))
|
20
|
+
# return early if we already have a copy of this object stored.
|
21
|
+
# if this object was previously uploaded, we use its URLs (and not, for example,
|
22
|
+
# do a local copy to the requested path) because this way cloudformation can see
|
23
|
+
# that the updated template is exactly the same as the old one and will not force
|
24
|
+
# an unneeded update.
|
25
|
+
o = b.objects(prefix: "cf-compiled/#{prefix}/").first
|
26
|
+
if o.nil?
|
27
|
+
# no such luck, we need to actually upload the file
|
28
|
+
o = b.object("cf-compiled/#{prefix}/#{path}")
|
29
|
+
o.put acl: 'public-read',
|
30
|
+
body: content,
|
31
|
+
content_disposition: 'attachment',
|
32
|
+
content_encoding: 'gzip',
|
33
|
+
content_type: mime_type,
|
34
|
+
storage_class: 'REDUCED_REDUNDANCY'
|
35
|
+
else
|
36
|
+
log "re-using cached object"
|
37
|
+
end
|
38
|
+
o.public_url
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloudformation-tool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oded Arbel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2011-03-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: clamp
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: autoloaded
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2'
|
55
|
+
description:
|
56
|
+
email: oded.arbel@greenfieldtech.net
|
57
|
+
executables:
|
58
|
+
- cftool
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files:
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
files:
|
64
|
+
- LICENSE
|
65
|
+
- README.md
|
66
|
+
- bin/cftool
|
67
|
+
- lib/cloud_formation_tool.rb
|
68
|
+
- lib/cloud_formation_tool/cli.rb
|
69
|
+
- lib/cloud_formation_tool/cli/compile.rb
|
70
|
+
- lib/cloud_formation_tool/cli/create.rb
|
71
|
+
- lib/cloud_formation_tool/cli/delete.rb
|
72
|
+
- lib/cloud_formation_tool/cli/list_stacks.rb
|
73
|
+
- lib/cloud_formation_tool/cli/main.rb
|
74
|
+
- lib/cloud_formation_tool/cli/monitor.rb
|
75
|
+
- lib/cloud_formation_tool/cli/parameters.rb
|
76
|
+
- lib/cloud_formation_tool/cli/servers.rb
|
77
|
+
- lib/cloud_formation_tool/cli/status.rb
|
78
|
+
- lib/cloud_formation_tool/cloud_formation.rb
|
79
|
+
- lib/cloud_formation_tool/cloud_formation/lambda_code.rb
|
80
|
+
- lib/cloud_formation_tool/cloud_formation/stack.rb
|
81
|
+
- lib/cloud_formation_tool/cloud_init.rb
|
82
|
+
- lib/cloud_formation_tool/errors.rb
|
83
|
+
- lib/cloud_formation_tool/storable.rb
|
84
|
+
- lib/cloud_formation_tool/version.rb
|
85
|
+
homepage: http://github.com/GreenfieldTech/cloudformation-tool
|
86
|
+
licenses:
|
87
|
+
- GPL-2.0
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.4.5
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: A pre-compiler tool for CloudFormation YAML templates
|
109
|
+
test_files: []
|