steam_donkey 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +76 -0
- data/README.md +161 -0
- data/README.rdoc +6 -0
- data/Rakefile +54 -0
- data/bin/donkey +43 -0
- data/features/steam_donkey.feature +8 -0
- data/features/step_definitions/steam_donkey_steps.rb +6 -0
- data/features/support/env.rb +15 -0
- data/fixtures/templates/1/child1.template +2 -0
- data/fixtures/templates/child2.template +2 -0
- data/fixtures/templates/parent.template +2 -0
- data/lib/steam_donkey.rb +12 -0
- data/lib/steam_donkey/aws.rb +20 -0
- data/lib/steam_donkey/cli/output.rb +62 -0
- data/lib/steam_donkey/cloudformation/deploy_stack.rb +15 -0
- data/lib/steam_donkey/cloudformation/event_log.rb +108 -0
- data/lib/steam_donkey/cloudformation/exports_listing.rb +49 -0
- data/lib/steam_donkey/cloudformation/package.rb +100 -0
- data/lib/steam_donkey/cloudformation/stack_listing.rb +47 -0
- data/lib/steam_donkey/commands/cf.rb +102 -0
- data/lib/steam_donkey/commands/ec2.rb +31 -0
- data/lib/steam_donkey/config.rb +14 -0
- data/lib/steam_donkey/ec2/instance_listing.rb +54 -0
- data/lib/steam_donkey/resource_listing.rb +152 -0
- data/lib/steam_donkey/version.rb +3 -0
- data/steam_donkey.gemspec +28 -0
- data/steam_donkey.rdoc +5 -0
- data/test/default_test.rb +14 -0
- data/test/test_helper.rb +9 -0
- metadata +208 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
def client_options(options)
|
4
|
+
result = {}
|
5
|
+
result[:profile] = options[:profile] if !options[:profile].nil?
|
6
|
+
result[:region] = options[:region] if !options[:region].nil?
|
7
|
+
result
|
8
|
+
end
|
9
|
+
|
10
|
+
def cf_client(options)
|
11
|
+
Aws::CloudFormation::Client.new(client_options(options))
|
12
|
+
end
|
13
|
+
|
14
|
+
def ec2_client(options)
|
15
|
+
Aws::EC2::Client.new(client_options(options))
|
16
|
+
end
|
17
|
+
|
18
|
+
def s3_client(options)
|
19
|
+
Aws::S3::Client.new(client_options(options))
|
20
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'command_line_reporter'
|
2
|
+
|
3
|
+
module SteamDonkey
|
4
|
+
module Cli
|
5
|
+
class Output
|
6
|
+
include CommandLineReporter
|
7
|
+
|
8
|
+
def initialize(render_headings, format)
|
9
|
+
@format = format
|
10
|
+
@render_headings = render_headings
|
11
|
+
end
|
12
|
+
|
13
|
+
def render(headings, rows)
|
14
|
+
case @format
|
15
|
+
when 'pretty'
|
16
|
+
table do
|
17
|
+
row :header => true do
|
18
|
+
headings.each do |label|
|
19
|
+
column label, :width => max_width(rows, label)+ 2, :color => 'blue'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
rows.each do |result|
|
24
|
+
row do
|
25
|
+
headings.each do |label|
|
26
|
+
value = result.detect { |f| f[:label] == label }[:value]
|
27
|
+
value = '-' if value.nil?
|
28
|
+
if value.is_a? Time
|
29
|
+
value = value.iso8601.to_s
|
30
|
+
else
|
31
|
+
value = '-' if value.methods.include? :empty? and value.empty?
|
32
|
+
end
|
33
|
+
column value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
else
|
39
|
+
puts headings.join(',') if @render_headings
|
40
|
+
rows.each do |result|
|
41
|
+
puts result.map { |c| c[:value] }.join(',')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def max_width(results, label)
|
47
|
+
width = results.map do |result|
|
48
|
+
value = result.detect { |f| f[:label] == label }[:value]
|
49
|
+
if value.nil?
|
50
|
+
0
|
51
|
+
elsif value.is_a? Time
|
52
|
+
value.iso8601.to_s.length
|
53
|
+
else
|
54
|
+
value.to_s.length
|
55
|
+
end
|
56
|
+
end
|
57
|
+
width << label.length
|
58
|
+
width.max
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module SteamDonkey
|
2
|
+
module Cloudformation
|
3
|
+
class EventLog
|
4
|
+
|
5
|
+
def initialize(client, options)
|
6
|
+
@client = client
|
7
|
+
@options = options
|
8
|
+
@events_seen = []
|
9
|
+
end
|
10
|
+
|
11
|
+
BOLD = "[1m"
|
12
|
+
BLACK = "[30m"
|
13
|
+
RED = "[31m"
|
14
|
+
GREEN = "[32m"
|
15
|
+
YELLOW = "[33m"
|
16
|
+
BLUE = "[34m"
|
17
|
+
MAGENTA = "[35m"
|
18
|
+
CYAN = "[36m"
|
19
|
+
WHITE = "[37m"
|
20
|
+
|
21
|
+
def status_colors
|
22
|
+
{
|
23
|
+
"REVIEW_IN_PROGRESS" => "\033" + YELLOW,
|
24
|
+
|
25
|
+
"CREATE_IN_PROGRESS" => "\033" + YELLOW,
|
26
|
+
"CREATE_FAILED" => "\033" + RED,
|
27
|
+
"CREATE_COMPLETE" => "\033" + GREEN,
|
28
|
+
|
29
|
+
"DELETE_IN_PROGRESS" => "\033" + RED,
|
30
|
+
"DELETE_FAILED" => "\033" + RED,
|
31
|
+
"DELETE_COMPLETE" => "\033" + BOLD,
|
32
|
+
"DELETE_SKIPPED" => "\033" + BOLD,
|
33
|
+
|
34
|
+
"UPDATE_IN_PROGRESS" => "\033" + YELLOW,
|
35
|
+
"UPDATE_FAILED" => "\033" + RED,
|
36
|
+
"UPDATE_COMPLETE" => "\033" + GREEN,
|
37
|
+
"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS" => "\033" + YELLOW,
|
38
|
+
|
39
|
+
"UPDATE_ROLLBACK_IN_PROGRESS" => "\033" + RED,
|
40
|
+
"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" => "\033" + RED,
|
41
|
+
"UPDATE_ROLLBACK_COMPLETE" => "\033" + GREEN
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def list
|
46
|
+
printf("%s%-20s %-44s %-26s %s%s\n",
|
47
|
+
"\033[1m",
|
48
|
+
"Timestamp",
|
49
|
+
"Status",
|
50
|
+
"Resource Type",
|
51
|
+
"Logical Id",
|
52
|
+
"\033[0m"
|
53
|
+
)
|
54
|
+
|
55
|
+
trap("SIGINT") { throw :ctrl_c }
|
56
|
+
catch :ctrl_c do
|
57
|
+
begin
|
58
|
+
events = []
|
59
|
+
stack_events.each do |response|
|
60
|
+
new_events(response).each do |event|
|
61
|
+
@events_seen << event.event_id
|
62
|
+
if after_desired_time(event)
|
63
|
+
events.unshift event
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
print_events(events)
|
68
|
+
sleep 5 if @options[:follow]
|
69
|
+
end while @options[:follow]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def after_desired_time(event)
|
74
|
+
event.timestamp > @options[:since]
|
75
|
+
end
|
76
|
+
|
77
|
+
def stack_name
|
78
|
+
@options[:stack_name]
|
79
|
+
end
|
80
|
+
|
81
|
+
def stack_events
|
82
|
+
@client.describe_stack_events(:stack_name => stack_name)
|
83
|
+
end
|
84
|
+
|
85
|
+
def new_events(response)
|
86
|
+
response.stack_events.select(&self.method(:is_new?))
|
87
|
+
end
|
88
|
+
|
89
|
+
def is_new?(event)
|
90
|
+
!@events_seen.include? event.event_id
|
91
|
+
end
|
92
|
+
|
93
|
+
def print_events(events)
|
94
|
+
events.each do |event|
|
95
|
+
# puts "#{event.timestamp.iso8601}"
|
96
|
+
printf("%-20s %s%-44s%s %-26s %s\n",
|
97
|
+
event.timestamp.iso8601,
|
98
|
+
status_colors[event.resource_status], event.resource_status, "\033[0m",
|
99
|
+
event.resource_type,
|
100
|
+
event.logical_resource_id
|
101
|
+
)
|
102
|
+
end
|
103
|
+
STDOUT.flush
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module SteamDonkey
|
4
|
+
module Cloudformation
|
5
|
+
class ExportsListing
|
6
|
+
include SteamDonkey::ResourceListing
|
7
|
+
|
8
|
+
def initialize(client, options = {})
|
9
|
+
@client = client
|
10
|
+
init(options[:sort], options[:filters], options[:columns])
|
11
|
+
end
|
12
|
+
|
13
|
+
def aliases
|
14
|
+
[
|
15
|
+
{ test: /^StackId$/i, value: 'ExportingStackId' }
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def search
|
20
|
+
result = []
|
21
|
+
exports = @client.list_exports
|
22
|
+
begin
|
23
|
+
last_token = exports.next_token
|
24
|
+
exports.exports.map do |export|
|
25
|
+
result << export
|
26
|
+
end
|
27
|
+
exports = @client.list_exports({ :next_token => exports.next_token })
|
28
|
+
end while last_token != exports.next_token
|
29
|
+
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def select_column(column, stack)
|
34
|
+
begin
|
35
|
+
c = column.clone
|
36
|
+
case column[:name]
|
37
|
+
when 'nil'
|
38
|
+
else
|
39
|
+
c[:value] = stack.send(column[:name].underscore)
|
40
|
+
end
|
41
|
+
c
|
42
|
+
rescue
|
43
|
+
raise "Unknown column #{column[:name]}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module SteamDonkey
|
4
|
+
module Cloudformation
|
5
|
+
|
6
|
+
|
7
|
+
class Template
|
8
|
+
|
9
|
+
def initialize(dirname, filename)
|
10
|
+
@dirname = dirname
|
11
|
+
@filename = filename
|
12
|
+
end
|
13
|
+
|
14
|
+
def path
|
15
|
+
File.join(@dirname, @filename)
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
@filename
|
20
|
+
end
|
21
|
+
|
22
|
+
def contains_child_templates?
|
23
|
+
!templates.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def templates
|
27
|
+
@child_references ||= scan_for_child_templates path
|
28
|
+
end
|
29
|
+
|
30
|
+
def relative_path(root)
|
31
|
+
template_path = Pathname.new(path)
|
32
|
+
root_path = Pathname.new(root)
|
33
|
+
template_path.relative_path_from(root_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def packaged_template(bucket, prefix, root_path)
|
37
|
+
lines = []
|
38
|
+
File.readlines(path).each do |line|
|
39
|
+
new_line = line
|
40
|
+
line.scan /\.\/(.+)$/ do |child_path|
|
41
|
+
unless child_path.empty?
|
42
|
+
new_line = line.gsub(/\.\/.+$/, "https://s3-eu-west-1.amazonaws.com/#{bucket}/#{prefix}/#{child_path.first}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
lines << new_line
|
46
|
+
end
|
47
|
+
lines.join
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def scan_for_child_templates(template)
|
53
|
+
child_templates = []
|
54
|
+
child_templates << self
|
55
|
+
File.readlines(template).each do |line|
|
56
|
+
line.scan /\.\/(.+)$/ do |child_path|
|
57
|
+
unless child_path.empty?
|
58
|
+
child = create_child_template(child_path.first)
|
59
|
+
child_templates << child
|
60
|
+
child_templates.push(*(child.templates))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
child_templates.uniq
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_child_template(path)
|
68
|
+
child_path = File.join(@dirname, path)
|
69
|
+
Template.new File.dirname(child_path), File.basename(child_path)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Package
|
74
|
+
def initialize(client, quiet, dry_run)
|
75
|
+
@client = client
|
76
|
+
@quiet = quiet || false
|
77
|
+
@dry_run = dry_run || false
|
78
|
+
end
|
79
|
+
|
80
|
+
def package(path, bucket, prefix)
|
81
|
+
template_dir = File.dirname(File.absolute_path(path))
|
82
|
+
template_name = File.basename(path)
|
83
|
+
|
84
|
+
root_template = Template.new(template_dir, template_name)
|
85
|
+
|
86
|
+
root_template.templates.each do |template|
|
87
|
+
puts "Uploading s3://#{bucket}/#{prefix}/#{template.relative_path(template_dir)}" unless @quiet
|
88
|
+
@client.put_object({
|
89
|
+
body: template.packaged_template(bucket, prefix, template_dir),
|
90
|
+
bucket: bucket,
|
91
|
+
key: "#{prefix}/#{template.relative_path(template_dir)}"
|
92
|
+
}) unless @dry_run
|
93
|
+
end
|
94
|
+
|
95
|
+
"https://s3-eu-west-1.amazonaws.com/#{bucket}/#{prefix}/#{root_template.name}"
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module SteamDonkey
|
4
|
+
module Cloudformation
|
5
|
+
class StackListing
|
6
|
+
include SteamDonkey::ResourceListing
|
7
|
+
|
8
|
+
def initialize(client, options = {})
|
9
|
+
@client = client
|
10
|
+
init(options[:sort], options[:filters], options[:columns])
|
11
|
+
end
|
12
|
+
|
13
|
+
def aliases
|
14
|
+
[
|
15
|
+
{ test: /^Id$/i, value: 'StackId' },
|
16
|
+
{ test: /^Name$/i, value: 'StackName' },
|
17
|
+
{ test: /^Status/i, value: 'StackStatus' },
|
18
|
+
{ test: /^StatusReason/i, value: 'StackStatusReason' },
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def search
|
23
|
+
@client.list_stacks.map do |response|
|
24
|
+
response.stack_summaries.map do |stack|
|
25
|
+
stack
|
26
|
+
end
|
27
|
+
end.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
def select_column(column, stack)
|
31
|
+
begin
|
32
|
+
c = column.clone
|
33
|
+
case column[:name]
|
34
|
+
when /^Tags\./i
|
35
|
+
c[:value] = find_tag(stack, column[:name].split('.').last)
|
36
|
+
else
|
37
|
+
c[:value] = stack.send(column[:name].underscore)
|
38
|
+
end
|
39
|
+
c
|
40
|
+
rescue
|
41
|
+
raise "Unknown column,#{column[:name]}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
|
3
|
+
desc 'Manage and view cloudformation stacks and templates'
|
4
|
+
command [:cf] do |cf|
|
5
|
+
|
6
|
+
cf.desc 'List cloudformation stacks'
|
7
|
+
cf.command [:list, :ls] do |list|
|
8
|
+
|
9
|
+
list.switch [:raw, :r], :default_value => false, :negatable => false, :desc => "Output unformatted, useful when piping results to other commands"
|
10
|
+
list.switch [:headings, :h], :default_value => false, :desc => "Toggle column headings"
|
11
|
+
|
12
|
+
list.flag [:filters, :f], :default_value => 'Status=!/DELETE_COMPLETE/', :desc => ""
|
13
|
+
list.flag [:columns, :c], :default_value => 'Name,CreationTime,Status', :desc => ""
|
14
|
+
list.flag [:sort, :s], :default_value => 'CreationTime=desc,Name', :desc => ""
|
15
|
+
|
16
|
+
list.flag [:output, :o], :default_value => 'pretty', :must_match => { "pretty" => :pretty, "raw" => :raw }
|
17
|
+
|
18
|
+
list.action do |global_options, options, args|
|
19
|
+
listing_options = {
|
20
|
+
:filters => options[:filters],
|
21
|
+
:columns => options[:columns],
|
22
|
+
:sort => options[:sort]
|
23
|
+
}
|
24
|
+
|
25
|
+
stack_listing = SteamDonkey::Cloudformation::StackListing.new(cf_client(global_options), listing_options)
|
26
|
+
|
27
|
+
output = SteamDonkey::Cli::Output.new true, options[:output]
|
28
|
+
output.render stack_listing.column_labels, stack_listing.list
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
cf.desc 'List stack events'
|
33
|
+
cf.command [:events] do |events|
|
34
|
+
|
35
|
+
events.flag ['stack-name', :s], :arg_name => 'STACK_NAME', :required => true, :desc => "Stack name"
|
36
|
+
events.switch ['follow', :f], :default_value => false, :negatable => false
|
37
|
+
|
38
|
+
events.action do |global_options, options, args|
|
39
|
+
list_options = {
|
40
|
+
:follow => options[:follow],
|
41
|
+
:stack_name => options[:'stack-name'],
|
42
|
+
:since => Chronic.parse("2 years ago")
|
43
|
+
}
|
44
|
+
|
45
|
+
event_log = SteamDonkey::Cloudformation::EventLog.new(cf_client(global_options), list_options)
|
46
|
+
event_log.list
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
cf.desc 'List cloudformation exports'
|
51
|
+
cf.command [:exports] do |exports|
|
52
|
+
|
53
|
+
exports.switch [:raw, :r], :default_value => false, :negatable => false, :desc => "Output unformatted, useful when piping results to other commands"
|
54
|
+
exports.switch [:headings, :h], :default_value => false, :desc => "Toggle column headings"
|
55
|
+
|
56
|
+
exports.flag [:filters, :f], :default_value => '', :desc => ""
|
57
|
+
exports.flag [:columns, :c], :default_value => 'Name,Value', :desc => ""
|
58
|
+
exports.flag [:sort, :s], :default_value => 'Name', :desc => ""
|
59
|
+
|
60
|
+
exports.flag [:output, :o], :default_value => 'pretty', :must_match => { "pretty" => :pretty, "raw" => :raw }
|
61
|
+
|
62
|
+
exports.action do |global_options, options, args|
|
63
|
+
listing_options = {
|
64
|
+
:filters => options[:filters],
|
65
|
+
:columns => options[:columns],
|
66
|
+
:sort => options[:sort]
|
67
|
+
}
|
68
|
+
|
69
|
+
exports_listing = SteamDonkey::Cloudformation::ExportsListing.new(cf_client(global_options), listing_options)
|
70
|
+
|
71
|
+
output = SteamDonkey::Cli::Output.new true, options[:output]
|
72
|
+
output.render exports_listing.column_labels, exports_listing.list
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
cf.desc 'Package cloudformation template and upload to S3'
|
77
|
+
cf.command [:package] do |p|
|
78
|
+
|
79
|
+
p.flag [:template, :t], :required => true, :desc => "Path to template to package and upload"
|
80
|
+
p.flag [:bucket, :b], :desc => "Name of the S3 bucket to upload packaged templates to"
|
81
|
+
p.flag [:prefix, :p], :desc => "Prefix to prepend to uploaded templates"
|
82
|
+
p.switch ['launch-console', :l], :default_value => false, :negatable => false, :desc => "Launch CloudFormation console with uploaded template"
|
83
|
+
p.switch ['dry-run', :d], :default_value => false, :negatable => false
|
84
|
+
p.switch [:quiet, :q], :default_value => false, :negatable => false
|
85
|
+
|
86
|
+
p.action do |global_options, options, args|
|
87
|
+
bucket_name = options[:bucket] || global_options[:rc][:cloudformation][:package]["bucketName"]
|
88
|
+
bucket_path_prefix = options[:prefix] || global_options[:rc][:cloudformation][:package]["bucketPathPrefix"]
|
89
|
+
|
90
|
+
package = SteamDonkey::Cloudformation::Package.new(s3_client(global_options), options[:quiet], options['dry-run'])
|
91
|
+
|
92
|
+
template_url = package.package options[:template], bucket_name, bucket_path_prefix
|
93
|
+
|
94
|
+
puts template_url unless options[:quiet]
|
95
|
+
|
96
|
+
if options['launch-console']
|
97
|
+
`open https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?templateURL=#{template_url}`
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|