sfn 0.0.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -0
- data/LICENSE +13 -0
- data/README.md +142 -61
- data/bin/sfn +43 -0
- data/lib/chef/knife/knife_plugin_seed.rb +117 -0
- data/lib/sfn.rb +17 -0
- data/lib/sfn/cache.rb +385 -0
- data/lib/sfn/command.rb +45 -0
- data/lib/sfn/command/create.rb +87 -0
- data/lib/sfn/command/describe.rb +87 -0
- data/lib/sfn/command/destroy.rb +74 -0
- data/lib/sfn/command/events.rb +98 -0
- data/lib/sfn/command/export.rb +103 -0
- data/lib/sfn/command/import.rb +117 -0
- data/lib/sfn/command/inspect.rb +160 -0
- data/lib/sfn/command/list.rb +59 -0
- data/lib/sfn/command/promote.rb +17 -0
- data/lib/sfn/command/update.rb +95 -0
- data/lib/sfn/command/validate.rb +34 -0
- data/lib/sfn/command_module.rb +9 -0
- data/lib/sfn/command_module/base.rb +150 -0
- data/lib/sfn/command_module/stack.rb +166 -0
- data/lib/sfn/command_module/template.rb +147 -0
- data/lib/sfn/config.rb +106 -0
- data/lib/sfn/config/create.rb +35 -0
- data/lib/sfn/config/describe.rb +19 -0
- data/lib/sfn/config/destroy.rb +9 -0
- data/lib/sfn/config/events.rb +25 -0
- data/lib/sfn/config/export.rb +29 -0
- data/lib/sfn/config/import.rb +24 -0
- data/lib/sfn/config/inspect.rb +37 -0
- data/lib/sfn/config/list.rb +25 -0
- data/lib/sfn/config/promote.rb +23 -0
- data/lib/sfn/config/update.rb +20 -0
- data/lib/sfn/config/validate.rb +49 -0
- data/lib/sfn/monkey_patch.rb +8 -0
- data/lib/sfn/monkey_patch/stack.rb +200 -0
- data/lib/sfn/provider.rb +224 -0
- data/lib/sfn/utils.rb +23 -0
- data/lib/sfn/utils/debug.rb +31 -0
- data/lib/sfn/utils/json.rb +37 -0
- data/lib/sfn/utils/object_storage.rb +28 -0
- data/lib/sfn/utils/output.rb +79 -0
- data/lib/sfn/utils/path_selector.rb +99 -0
- data/lib/sfn/utils/ssher.rb +29 -0
- data/lib/sfn/utils/stack_exporter.rb +275 -0
- data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
- data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
- data/lib/sfn/version.rb +4 -0
- data/sfn.gemspec +19 -0
- metadata +110 -4
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'sfn'
|
3
|
+
|
4
|
+
module Sfn
|
5
|
+
class Command
|
6
|
+
# Import command
|
7
|
+
class Import < Command
|
8
|
+
|
9
|
+
include Sfn::CommandModule::Base
|
10
|
+
include Sfn::Utils::JSON
|
11
|
+
include Sfn::Utils::ObjectStorage
|
12
|
+
include Sfn::Utils::PathSelector
|
13
|
+
|
14
|
+
# Run the import action
|
15
|
+
def execute!
|
16
|
+
raise NotImplementedError.new 'Implementation updates required'
|
17
|
+
stack_name, json_file = name_args
|
18
|
+
ui.info "#{ui.color('Stack Import:', :bold)} #{stack_name}"
|
19
|
+
unless(json_file)
|
20
|
+
entries = [].tap do |_entries|
|
21
|
+
_entries.push('s3') if config[:bucket]
|
22
|
+
_entries.push('fs') if config[:path]
|
23
|
+
end
|
24
|
+
if(entries.size > 1)
|
25
|
+
valid = false
|
26
|
+
until(valid)
|
27
|
+
answer = ui.ask_question('Import via file system (fs) or remote bucket (remote)?', :default => 'remote')
|
28
|
+
valid = true if %w(remote fs).include?(answer)
|
29
|
+
entries = [answer]
|
30
|
+
end
|
31
|
+
elsif(entries.size < 1)
|
32
|
+
ui.fatal 'No path or bucket set. Unable to perform dynamic lookup!'
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
case entries.first
|
36
|
+
when 'remote'
|
37
|
+
json_file = remote_discovery
|
38
|
+
else
|
39
|
+
json_file = local_discovery
|
40
|
+
end
|
41
|
+
end
|
42
|
+
if(File.exists?(json_file) || json_file.is_a?(IO))
|
43
|
+
content = json_file.is_a?(IO) ? json_file.read : File.read(json_file)
|
44
|
+
export = Mash.new(_from_json(content))
|
45
|
+
begin
|
46
|
+
creator = namespace.const_val(:Create).new(
|
47
|
+
Smash.new(
|
48
|
+
:template => _from_json(export[:stack][:template]),
|
49
|
+
:options => _from_json(export[:stack][:options])
|
50
|
+
),
|
51
|
+
[stack_name]
|
52
|
+
)
|
53
|
+
ui.info ' - Starting creation of import'
|
54
|
+
creator.execute!
|
55
|
+
ui.info "#{ui.color('Stack Import', :bold)} (#{json_file}): #{ui.color('complete', :green)}"
|
56
|
+
rescue => e
|
57
|
+
ui.fatal "Failed to import stack: #{e}"
|
58
|
+
debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
else
|
62
|
+
ui.fatal "Failed to locate JSON export file (#{json_file})"
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Generate bucket prefix
|
68
|
+
#
|
69
|
+
# @return [String, NilClass]
|
70
|
+
def bucket_prefix
|
71
|
+
if(prefix = config[:bucket_prefix])
|
72
|
+
if(prefix.respond_to?(:call))
|
73
|
+
prefix.call
|
74
|
+
else
|
75
|
+
prefix.to_s
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Discover remote file
|
81
|
+
#
|
82
|
+
# @return [IO] stack export IO
|
83
|
+
def remote_discovery
|
84
|
+
storage = provider.service_for(:storage)
|
85
|
+
directory = storage.directories.get(config[:bucket])
|
86
|
+
file = prompt_for_file(
|
87
|
+
directory,
|
88
|
+
:directories_name => 'Collections',
|
89
|
+
:files_names => 'Exports',
|
90
|
+
:filter_prefix => bucket_prefix
|
91
|
+
)
|
92
|
+
if(file)
|
93
|
+
remote_file = storage.files.get(file)
|
94
|
+
StringIO.new(remote_file.body)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Discover remote file
|
99
|
+
#
|
100
|
+
# @return [IO] stack export IO
|
101
|
+
def local_discovery
|
102
|
+
_, bucket = config[:path].split('/', 2)
|
103
|
+
storage = provider.service_for(:storage,
|
104
|
+
:provider => :local,
|
105
|
+
:local_root => '/'
|
106
|
+
)
|
107
|
+
directory = storage.directories.get(bucket)
|
108
|
+
prompt_for_file(
|
109
|
+
directory,
|
110
|
+
:directories_name => 'Collections',
|
111
|
+
:files_names => 'Exports'
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
# Inspect command
|
6
|
+
class Inspect < Command
|
7
|
+
|
8
|
+
include Sfn::CommandModule::Base
|
9
|
+
include Sfn::Utils::Ssher
|
10
|
+
|
11
|
+
# Run the stack inspection action
|
12
|
+
def execute!
|
13
|
+
stack_name = name_args.last
|
14
|
+
stack = provider.connection.stacks.get(stack_name)
|
15
|
+
ui.info "Stack inspection #{ui.color(stack_name, :bold)}:"
|
16
|
+
outputs = [:attribute, :nodes, :instance_failure].map do |key|
|
17
|
+
if(config.has_key?(key))
|
18
|
+
send("display_#{key}", stack)
|
19
|
+
key
|
20
|
+
end
|
21
|
+
end.compact
|
22
|
+
if(outputs.empty?)
|
23
|
+
ui.info ' Stack dump:'
|
24
|
+
ui.puts MultiJson.dump(
|
25
|
+
MultiJson.load(
|
26
|
+
stack.reload.to_json
|
27
|
+
),
|
28
|
+
:pretty => true
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def display_instance_failure(stack)
|
34
|
+
instances = stack.resources.all.find_all do |resource|
|
35
|
+
resource.state.to_s.end_with?('failed')
|
36
|
+
end.map do |resource|
|
37
|
+
# If compute instance, simply expand
|
38
|
+
if(resource.within?(:compute, :servers))
|
39
|
+
resource.instance
|
40
|
+
# If a waitcondition, check for instance ID
|
41
|
+
elsif(resource.type.to_s.downcase.end_with?('waitcondition'))
|
42
|
+
if(resource.status_reason.to_s.include?('uniqueId'))
|
43
|
+
srv_id = resource.status_reason.split(' ').last.strip
|
44
|
+
provider.connection.api_for(:compute).servers.get(srv_id)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end.compact
|
48
|
+
if(instances.empty?)
|
49
|
+
ui.error 'Failed to locate any failed instances'
|
50
|
+
else
|
51
|
+
log_path = config[:failure_log_path]
|
52
|
+
if(log_path.to_s.empty?)
|
53
|
+
log_path = '/var/log/chef/client.log'
|
54
|
+
end
|
55
|
+
opts = ssh_key ? {:keys => [ssh_key]} : {}
|
56
|
+
instances.each do |instance|
|
57
|
+
ui.info " -> Log inspect for #{instance.id}:"
|
58
|
+
address = instance.addresses_public.map do |address|
|
59
|
+
if(address.version == 4)
|
60
|
+
address.address
|
61
|
+
end
|
62
|
+
end
|
63
|
+
if(address)
|
64
|
+
ssh_attempt_users.each do |user|
|
65
|
+
begin
|
66
|
+
ui.info remote_file_contents(address.first, user, log_path, opts)
|
67
|
+
break
|
68
|
+
rescue Net::SSH::AuthenticationFailed
|
69
|
+
ui.warn "Authentication failed for user #{user} on instance #{address}"
|
70
|
+
rescue => e
|
71
|
+
ui.error "Failed to retrieve log: #{e}"
|
72
|
+
_debug e
|
73
|
+
break
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Users to attempt SSH connection
|
82
|
+
#
|
83
|
+
# @return [Array<String>] usernames for ssh connect attempt
|
84
|
+
def ssh_attempt_users
|
85
|
+
[config[:ssh_user], config[:ssh_attempt_users], ENV['USER']].flatten.compact.uniq
|
86
|
+
end
|
87
|
+
|
88
|
+
def ssh_key
|
89
|
+
config[:identity_file]
|
90
|
+
end
|
91
|
+
|
92
|
+
def display_attribute(stack)
|
93
|
+
attr = config[:attribute].split('.').inject(stack) do |memo, key|
|
94
|
+
args = key.scan(/\(([^)]*)\)/).flatten.first.to_s
|
95
|
+
if(args)
|
96
|
+
args = args.split(',').map{|a| a.to_i.to_s == a ? a.to_i : a}
|
97
|
+
key = key.split('(').first
|
98
|
+
end
|
99
|
+
if(memo.public_methods.include?(key.to_sym))
|
100
|
+
if(args.size == 1 && args.first.to_s.start_with?('&'))
|
101
|
+
memo.send(key, &args.first.slice(2, args.first.size).to_sym)
|
102
|
+
else
|
103
|
+
memo.send(*[key, args].flatten.compact)
|
104
|
+
end
|
105
|
+
else
|
106
|
+
raise NoMethodError.new "Invalid attribute requested! (#{memo.class}##{key})"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
ui.info " Attribute Lookup -> #{config[:attribute]}:"
|
110
|
+
ui.puts MultiJson.dump(
|
111
|
+
MultiJson.load(
|
112
|
+
MultiJson.dump(attr)
|
113
|
+
),
|
114
|
+
:pretty => true
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def display_nodes(stack)
|
119
|
+
asg_nodes = Smash[
|
120
|
+
stack.resources.all.find_all do |resource|
|
121
|
+
resource.within?(:auto_scale, :groups)
|
122
|
+
end.map do |group_resource|
|
123
|
+
asg = group_resource.expand
|
124
|
+
[
|
125
|
+
asg.name,
|
126
|
+
Smash[
|
127
|
+
asg.servers.map(&:expand).map{|s|
|
128
|
+
[s.id, Smash.new(
|
129
|
+
:name => s.name,
|
130
|
+
:addresses => s.addresses.map(&:address)
|
131
|
+
)]
|
132
|
+
}
|
133
|
+
]
|
134
|
+
]
|
135
|
+
end
|
136
|
+
]
|
137
|
+
compute_nodes = Smash[
|
138
|
+
stack.resources.all.find_all do |resource|
|
139
|
+
resource.within?(:compute, :servers)
|
140
|
+
end.map do |srv|
|
141
|
+
srv = srv.instance
|
142
|
+
[srv.id, Smash.new(
|
143
|
+
:name => srv.name,
|
144
|
+
:addresses => srv.addresses.map(&:address)
|
145
|
+
)]
|
146
|
+
end
|
147
|
+
]
|
148
|
+
unless(asg_nodes.empty?)
|
149
|
+
ui.info ' AutoScale Group Instances:'
|
150
|
+
ui.puts MultiJson.dump(asg_nodes, :pretty => true)
|
151
|
+
end
|
152
|
+
unless(compute_nodes.empty?)
|
153
|
+
ui.info ' Compute Instances:'
|
154
|
+
ui.puts MultiJson.dump(compute_nodes, :pretty => true)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
# List command
|
6
|
+
class List < Command
|
7
|
+
|
8
|
+
include Sfn::CommandModule::Base
|
9
|
+
|
10
|
+
# Run the list command
|
11
|
+
def execute!
|
12
|
+
ui.table(self) do
|
13
|
+
table(:border => false) do
|
14
|
+
stacks = get_stacks
|
15
|
+
row(:header => true) do
|
16
|
+
allowed_attributes.each do |attr|
|
17
|
+
column attr.split('_').map(&:capitalize).join(' '), :width => stacks.map{|s| s[attr].to_s.length}.max + 2
|
18
|
+
end
|
19
|
+
end
|
20
|
+
get_stacks.each do |stack|
|
21
|
+
row do
|
22
|
+
allowed_attributes.each do |attr|
|
23
|
+
column stack[attr]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end.display
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the list of stacks to display
|
32
|
+
#
|
33
|
+
# @return [Array<Hash>]
|
34
|
+
def get_stacks
|
35
|
+
provider.stacks.all.map do |stack|
|
36
|
+
Smash.new(stack.attributes)
|
37
|
+
end.sort do |x, y|
|
38
|
+
if(y[:created].to_s.empty?)
|
39
|
+
-1
|
40
|
+
elsif(x[:created].to_s.empty?)
|
41
|
+
1
|
42
|
+
else
|
43
|
+
Time.parse(x[:created].to_s) <=> Time.parse(y[:created].to_s)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Array<String>] default attributes to display
|
49
|
+
def default_attributes
|
50
|
+
if(provider.connection.provider == :aws)
|
51
|
+
%w(name created status template_description)
|
52
|
+
else
|
53
|
+
%w(name created status description)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
# Promote command
|
6
|
+
class Promote < Command
|
7
|
+
|
8
|
+
include Sfn::CommandModule::Base
|
9
|
+
|
10
|
+
def execute!
|
11
|
+
raise NotImplementedError.new 'Implementation updates required'
|
12
|
+
stack_name, destination = name_args
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
class Command
|
5
|
+
# Update command
|
6
|
+
class Update < Command
|
7
|
+
|
8
|
+
include Sfn::CommandModule::Base
|
9
|
+
include Sfn::CommandModule::Template
|
10
|
+
include Sfn::CommandModule::Stack
|
11
|
+
|
12
|
+
# Run the stack creation command
|
13
|
+
def execute!
|
14
|
+
name = name_args.first
|
15
|
+
unless(name)
|
16
|
+
ui.fatal "Formation name must be specified!"
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
stack_info = "#{ui.color('Name:', :bold)} #{name}"
|
21
|
+
|
22
|
+
if(config[:file])
|
23
|
+
file = load_template_file
|
24
|
+
stack_info << " #{ui.color('Path:', :bold)} #{config[:file]}"
|
25
|
+
nested_stacks = file.delete('sfn_nested_stack')
|
26
|
+
end
|
27
|
+
|
28
|
+
if(nested_stacks)
|
29
|
+
|
30
|
+
unpack_nesting(name, file, :update)
|
31
|
+
|
32
|
+
else
|
33
|
+
stack = provider.connection.stacks.get(name)
|
34
|
+
|
35
|
+
if(stack)
|
36
|
+
ui.info "#{ui.color('Cloud Formation:', :bold)} #{ui.color('update', :green)}"
|
37
|
+
|
38
|
+
unless(file)
|
39
|
+
if(config[:template])
|
40
|
+
file = config[:template]
|
41
|
+
stack_info << " #{ui.color('(template provided)', :green)}"
|
42
|
+
else
|
43
|
+
stack_info << " #{ui.color('(no template update)', :yellow)}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
ui.info " -> #{stack_info}"
|
47
|
+
|
48
|
+
apply_stacks!(stack)
|
49
|
+
|
50
|
+
if(file)
|
51
|
+
populate_parameters!(file, stack.parameters)
|
52
|
+
stack.template = translate_template(file)
|
53
|
+
stack.parameters = config[:parameters]
|
54
|
+
stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
|
55
|
+
else
|
56
|
+
populate_parameters!(stack.template, stack.parameters)
|
57
|
+
stack.parameters = config[:parameters]
|
58
|
+
end
|
59
|
+
|
60
|
+
begin
|
61
|
+
stack.save
|
62
|
+
rescue Miasma::Error::ApiError::RequestError => e
|
63
|
+
if(e.message.downcase.include?('no updates')) # :'(
|
64
|
+
ui.warn "No updates detected for stack (#{stack.name})"
|
65
|
+
else
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if(config[:poll])
|
71
|
+
poll_stack(stack.name)
|
72
|
+
if(stack.success?)
|
73
|
+
ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
|
74
|
+
namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
|
75
|
+
else
|
76
|
+
ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
|
77
|
+
ui.info ""
|
78
|
+
namespace.const_get(:Inspect).new({:instance_failure => true}, [name]).execute!
|
79
|
+
raise
|
80
|
+
end
|
81
|
+
else
|
82
|
+
ui.warn 'Stack state polling has been disabled.'
|
83
|
+
ui.info "Stack update initialized for #{ui.color(name, :green)}"
|
84
|
+
end
|
85
|
+
else
|
86
|
+
ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
|
87
|
+
raise
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|