sfn 0.0.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -0
  3. data/LICENSE +13 -0
  4. data/README.md +142 -61
  5. data/bin/sfn +43 -0
  6. data/lib/chef/knife/knife_plugin_seed.rb +117 -0
  7. data/lib/sfn.rb +17 -0
  8. data/lib/sfn/cache.rb +385 -0
  9. data/lib/sfn/command.rb +45 -0
  10. data/lib/sfn/command/create.rb +87 -0
  11. data/lib/sfn/command/describe.rb +87 -0
  12. data/lib/sfn/command/destroy.rb +74 -0
  13. data/lib/sfn/command/events.rb +98 -0
  14. data/lib/sfn/command/export.rb +103 -0
  15. data/lib/sfn/command/import.rb +117 -0
  16. data/lib/sfn/command/inspect.rb +160 -0
  17. data/lib/sfn/command/list.rb +59 -0
  18. data/lib/sfn/command/promote.rb +17 -0
  19. data/lib/sfn/command/update.rb +95 -0
  20. data/lib/sfn/command/validate.rb +34 -0
  21. data/lib/sfn/command_module.rb +9 -0
  22. data/lib/sfn/command_module/base.rb +150 -0
  23. data/lib/sfn/command_module/stack.rb +166 -0
  24. data/lib/sfn/command_module/template.rb +147 -0
  25. data/lib/sfn/config.rb +106 -0
  26. data/lib/sfn/config/create.rb +35 -0
  27. data/lib/sfn/config/describe.rb +19 -0
  28. data/lib/sfn/config/destroy.rb +9 -0
  29. data/lib/sfn/config/events.rb +25 -0
  30. data/lib/sfn/config/export.rb +29 -0
  31. data/lib/sfn/config/import.rb +24 -0
  32. data/lib/sfn/config/inspect.rb +37 -0
  33. data/lib/sfn/config/list.rb +25 -0
  34. data/lib/sfn/config/promote.rb +23 -0
  35. data/lib/sfn/config/update.rb +20 -0
  36. data/lib/sfn/config/validate.rb +49 -0
  37. data/lib/sfn/monkey_patch.rb +8 -0
  38. data/lib/sfn/monkey_patch/stack.rb +200 -0
  39. data/lib/sfn/provider.rb +224 -0
  40. data/lib/sfn/utils.rb +23 -0
  41. data/lib/sfn/utils/debug.rb +31 -0
  42. data/lib/sfn/utils/json.rb +37 -0
  43. data/lib/sfn/utils/object_storage.rb +28 -0
  44. data/lib/sfn/utils/output.rb +79 -0
  45. data/lib/sfn/utils/path_selector.rb +99 -0
  46. data/lib/sfn/utils/ssher.rb +29 -0
  47. data/lib/sfn/utils/stack_exporter.rb +275 -0
  48. data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
  49. data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
  50. data/lib/sfn/version.rb +4 -0
  51. data/sfn.gemspec +19 -0
  52. 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