sfn 0.0.1 → 0.3.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.
- 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
|