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.
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