knife-cloudformation 0.1.22 → 0.2.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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +56 -2
  4. data/knife-cloudformation.gemspec +4 -7
  5. data/lib/chef/knife/cloudformation_create.rb +105 -245
  6. data/lib/chef/knife/cloudformation_describe.rb +50 -26
  7. data/lib/chef/knife/cloudformation_destroy.rb +17 -18
  8. data/lib/chef/knife/cloudformation_events.rb +48 -14
  9. data/lib/chef/knife/cloudformation_export.rb +117 -34
  10. data/lib/chef/knife/cloudformation_import.rb +124 -18
  11. data/lib/chef/knife/cloudformation_inspect.rb +159 -71
  12. data/lib/chef/knife/cloudformation_list.rb +20 -24
  13. data/lib/chef/knife/cloudformation_promote.rb +40 -0
  14. data/lib/chef/knife/cloudformation_update.rb +132 -15
  15. data/lib/chef/knife/cloudformation_validate.rb +35 -0
  16. data/lib/knife-cloudformation.rb +28 -0
  17. data/lib/knife-cloudformation/cache.rb +213 -35
  18. data/lib/knife-cloudformation/knife.rb +9 -0
  19. data/lib/knife-cloudformation/knife/base.rb +179 -0
  20. data/lib/knife-cloudformation/knife/stack.rb +94 -0
  21. data/lib/knife-cloudformation/knife/template.rb +174 -0
  22. data/lib/knife-cloudformation/monkey_patch.rb +8 -0
  23. data/lib/knife-cloudformation/monkey_patch/stack.rb +195 -0
  24. data/lib/knife-cloudformation/provider.rb +225 -0
  25. data/lib/knife-cloudformation/utils.rb +18 -98
  26. data/lib/knife-cloudformation/utils/animal_strings.rb +28 -0
  27. data/lib/knife-cloudformation/utils/debug.rb +31 -0
  28. data/lib/knife-cloudformation/utils/json.rb +64 -0
  29. data/lib/knife-cloudformation/utils/object_storage.rb +28 -0
  30. data/lib/knife-cloudformation/utils/output.rb +79 -0
  31. data/lib/knife-cloudformation/utils/path_selector.rb +99 -0
  32. data/lib/knife-cloudformation/utils/ssher.rb +29 -0
  33. data/lib/knife-cloudformation/utils/stack_exporter.rb +271 -0
  34. data/lib/knife-cloudformation/utils/stack_parameter_scrubber.rb +35 -0
  35. data/lib/knife-cloudformation/utils/stack_parameter_validator.rb +124 -0
  36. data/lib/knife-cloudformation/version.rb +2 -4
  37. metadata +47 -94
  38. data/Gemfile +0 -3
  39. data/Gemfile.lock +0 -90
  40. data/knife-cloudformation-0.1.20.gem +0 -0
  41. data/lib/knife-cloudformation/aws_commons.rb +0 -267
  42. data/lib/knife-cloudformation/aws_commons/stack.rb +0 -435
  43. data/lib/knife-cloudformation/aws_commons/stack_parameter_validator.rb +0 -79
  44. data/lib/knife-cloudformation/cloudformation_base.rb +0 -168
  45. data/lib/knife-cloudformation/export.rb +0 -174
@@ -1,35 +1,141 @@
1
- require 'knife-cloudformation/cloudformation_base'
1
+ require 'stringio'
2
+ require 'knife-cloudformation'
2
3
 
3
4
  class Chef
4
5
  class Knife
6
+ # Cloudformation import command
5
7
  class CloudformationImport < Knife
6
8
 
7
- include KnifeCloudformation::KnifeBase
9
+ include KnifeCloudformation::Knife::Base
10
+ include KnifeCloudformation::Utils::JSON
11
+ include KnifeCloudformation::Utils::ObjectStorage
12
+ include KnifeCloudformation::Utils::PathSelector
8
13
 
9
- banner 'knife cloudformation import NEW_STACK_NAME JSON_EXPORT_FILE'
14
+ banner 'knife cloudformation import NEW_STACK_NAME [JSON_EXPORT_FILE]'
10
15
 
11
- def run
16
+ option(:path,
17
+ :long => '--import-path PATH',
18
+ :description => 'Directory path JSON export files are located',
19
+ :proc => lambda{|v|
20
+ Chef::Config[:knife][:cloudformation][:import][:path] = File.expand_path(v)
21
+ }
22
+ )
23
+
24
+ option(:bucket,
25
+ :long => '--export-bucket BUCKET_NAME',
26
+ :description => 'Remote file bucket JSON export files are located',
27
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:import][:bucket] = v}
28
+ )
29
+
30
+ option(:bucket_prefix,
31
+ :long => '--bucket-key-prefix PREFIX',
32
+ :description => 'Key prefix for file storage in bucket. Can be callable block if defined within configuration',
33
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:import][:bucket_prefix] = v}
34
+ )
35
+
36
+ unless(Chef::Config[:knife][:cloudformation].has_key?(:import))
37
+ Chef::Config[:knife][:cloudformation][:import] = Mash.new
38
+ end
39
+
40
+ # Run the import action
41
+ def _run
12
42
  stack_name, json_file = name_args
13
- ui.info "#{ui.color('Stack Import', :bold)} (#{json_file})"
14
- if(File.exists?(json_file))
15
- stack = _from_json(File.read(json_file))
16
- creator = Chef::Knife::CloudformationCreate.new
17
- creator.name_args = [stack_name]
18
- Chef::Config[:knife][:cloudformation][:template] = stack['template_body']
19
- Chef::Config[:knife][:cloudformation][:options] = Mash.new
20
- Chef::Config[:knife][:cloudformation][:options][:parameters] = Mash.new
21
- stack['parameters'].each do |k,v|
22
- Chef::Config[:knife][:cloudformation][:options][:parameters][k] = v
43
+ ui.info "#{ui.color('Stack Import:', :bold)} #{stack_name}"
44
+ unless(json_file)
45
+ entries = [].tap do |_entries|
46
+ _entries.push('s3') if Chef::Config[:knife][:cloudformation][:import][:bucket]
47
+ _entries.push('fs') if Chef::Config[:knife][:cloudformation][:import][:path]
48
+ end
49
+ if(entries.size > 1)
50
+ valid = false
51
+ until(valid)
52
+ answer = ui.ask_question('Import via file system (fs) or remote bucket (remote)?', :default => 'remote')
53
+ valid = true if %w(remote fs).include?(answer)
54
+ entries = [answer]
55
+ end
56
+ elsif(entries.size < 1)
57
+ ui.fatal 'No path or bucket set. Unable to perform dynamic lookup!'
58
+ exit 1
59
+ end
60
+ case entries.first
61
+ when 'remote'
62
+ json_file = remote_discovery
63
+ else
64
+ json_file = local_discovery
65
+ end
66
+ end
67
+ if(File.exists?(json_file) || json_file.is_a?(IO))
68
+ content = json_file.is_a?(IO) ? json_file.read : File.read(json_file)
69
+ export = Mash.new(_from_json(content))
70
+ begin
71
+ creator = Chef::Knife::CloudformationCreate.new
72
+ creator.name_args = [stack_name]
73
+ Chef::Config[:knife][:cloudformation][:template] = _from_json(export[:stack][:template])
74
+ Chef::Config[:knife][:cloudformation][:options] = export[:stack][:options]
75
+ ui.info ' - Starting creation of import'
76
+ creator.run
77
+ ui.info "#{ui.color('Stack Import', :bold)} (#{json_file}): #{ui.color('complete', :green)}"
78
+ rescue => e
79
+ ui.fatal "Failed to import stack: #{e}"
80
+ debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
81
+ exit -1
23
82
  end
24
- ui.info ' - Starting creation of import'
25
- creator.run
26
- ui.info "#{ui.color('Stack Import', :bold)} (#{json_file}): #{ui.color('complete', :green)}"
27
83
  else
28
- ui.error "Failed to locate JSON export file (#{json_file})"
84
+ ui.fatal "Failed to locate JSON export file (#{json_file})"
29
85
  exit 1
30
86
  end
31
87
  end
32
88
 
89
+ # Generate bucket prefix
90
+ #
91
+ # @return [String, NilClass]
92
+ def bucket_prefix
93
+ if(prefix = Chef::Config[:knife][:cloudformation][:import][:bucket_prefix])
94
+ if(prefix.respond_to?(:cal))
95
+ prefix.call
96
+ else
97
+ prefix.to_s
98
+ end
99
+ end
100
+ end
101
+
102
+ # Discover remote file
103
+ #
104
+ # @return [IO] stack export IO
105
+ def remote_discovery
106
+ storage = provider.service_for(:storage)
107
+ directory = storage.directories.get(
108
+ Chef::Config[:knife][:cloudformation][:import][:bucket]
109
+ )
110
+ file = prompt_for_file(
111
+ directory,
112
+ :directories_name => 'Collections',
113
+ :files_names => 'Exports',
114
+ :filter_prefix => bucket_prefix
115
+ )
116
+ if(file)
117
+ remote_file = storage.files.get(file)
118
+ StringIO.new(remote_file.body)
119
+ end
120
+ end
121
+
122
+ # Discover remote file
123
+ #
124
+ # @return [IO] stack export IO
125
+ def local_discovery
126
+ _, bucket = Chef::Config[:knife][:cloudformation][:import][:path].split('/', 2)
127
+ storage = provider.service_for(:storage,
128
+ :provider => :local,
129
+ :local_root => '/'
130
+ )
131
+ directory = storage.directories.get(bucket)
132
+ prompt_for_file(
133
+ directory,
134
+ :directories_name => 'Collections',
135
+ :files_names => 'Exports'
136
+ )
137
+ end
138
+
33
139
  end
34
140
  end
35
141
  end
@@ -1,111 +1,133 @@
1
- require 'knife-cloudformation/cloudformation_base'
2
- require 'knife-cloudformation/utils'
1
+ require 'knife-cloudformation'
3
2
 
4
3
  class Chef
5
4
  class Knife
5
+ # Cloudformation inspect command
6
6
  class CloudformationInspect < Knife
7
7
 
8
- include KnifeCloudformation::KnifeBase
8
+ include KnifeCloudformation::Knife::Base
9
9
  include KnifeCloudformation::Utils::Ssher
10
10
 
11
11
  banner 'knife cloudformation inspect NAME'
12
12
 
13
- option(:instance_failure,
14
- :short => '-I',
15
- :long => '--instance-failure',
13
+ option(:attribute,
14
+ :short => '-a ATTR',
15
+ :long => '--attribute ATTR',
16
+ :description => 'Dot delimited attribute to view'
17
+ )
18
+
19
+ option(:nodes,
20
+ :short => '-N',
21
+ :long => '--[no-]nodes',
16
22
  :boolean => true,
17
- :description => 'Display log from failed instance'
23
+ :description => 'Locate all instances'
24
+ )
25
+
26
+ option(:instance_failure,
27
+ :short => '-I [LOG_FILE_PATH]',
28
+ :long => '--instance-failure [LOG_FILE_PATH]',
29
+ :descrption => 'Display chef log file (defaults /var/log/chef/client.log)',
30
+ :proc => lambda{|val|
31
+ Chef::Config[:knife][:cloudformation][:failure_log_path] = val
32
+ }
18
33
  )
19
34
 
20
35
  option(:identity_file,
21
36
  :short => '-i IDENTITY_FILE',
22
37
  :long => '--identity-file IDENTITY_FILE',
23
- :description => 'The SSH identity file used for authentication',
24
- :proc => lambda {|val|
38
+ :description => 'SSH identity file for authentication',
39
+ :proc => lambda{|val|
25
40
  Chef::Config[:knife][:cloudformation][:identity_file] = val
26
41
  }
27
42
  )
28
43
 
29
- option(:nodes,
30
- :short => '-N',
31
- :long => '--nodes',
32
- :boolean => true,
33
- :description => 'Display ec2 nodes of stack'
34
- )
35
-
36
44
  option(:ssh_user,
37
45
  :short => '-x SSH_USER',
38
46
  :long => '--ssh-user SSH_USER',
39
- :description => 'The ssh username',
40
- :proc => lambda {|val|
47
+ :description => 'SSH username for inspection connect',
48
+ :proc => lambda{|val|
41
49
  Chef::Config[:knife][:cloudformation][:ssh_user] = val
42
50
  }
43
51
  )
44
52
 
45
- def run
53
+ # Run the stack inspection action
54
+ def _run
46
55
  stack_name = name_args.last
47
- if(config[:instance_failure])
48
- do_instance_failure(stack_name)
49
- end
50
- if(config[:nodes])
51
- do_node_list(stack_name)
52
- end
53
- end
54
-
55
- def do_node_list(stack_name)
56
- nodes = stack(stack_name).nodes.map do |n|
57
- [n.id, n.public_ip_address]
58
- end.flatten
59
- ui.info "Nodes for stack: #{ui.color(stack_name, :bold)}"
60
- ui.info "#{ui.list(nodes, :uneven_columns_across, 2)}"
61
- end
62
-
63
- def do_instance_failure(stack_name)
64
- event = stack(stack_name).events.detect do |e|
65
- e['ResourceType'] == 'AWS::CloudFormation::WaitCondition' &&
66
- e['ResourceStatus'] == 'CREATE_FAILED' &&
67
- e['ResourceStatusReason'].include?('uniqueId')
68
- end
69
- if(event)
70
- process_instance_failure(stack_name, event)
71
- else
72
- ui.error "Failed to discover failed node within stack: #{stack_name}"
73
- exit 1
56
+ stack = provider.stacks.get(stack_name)
57
+ ui.info "Stack inspection #{ui.color(stack_name, :bold)}:"
58
+ outputs = [:attribute, :nodes, :instance_failure].map do |key|
59
+ if(config.has_key?(key))
60
+ send("display_#{key}", stack)
61
+ key
62
+ end
63
+ end.compact
64
+ if(outputs.empty?)
65
+ ui.info ' Stack dump:'
66
+ ui.info MultiJson.dump(
67
+ MultiJson.load(
68
+ stack.reload.to_json
69
+ ),
70
+ :pretty => true
71
+ )
74
72
  end
75
73
  end
76
74
 
77
- def process_instance_failure(stack_name, event)
78
- inst_id = event['ResourceStatusReason'].split(' ').last.strip
79
- inst_addr = aws.aws(:ec2).servers.get(inst_id).public_ip_address
80
- ui.info "Displaying stack #{ui.color(stack_name, :bold)} failure on instance #{ui.color(inst_id, :bold)}"
81
- opts = ssh_key ? {:keys => [ssh_key]} : {}
82
- remote_path = '/var/log/cfn-init.log'
83
- content = nil
84
- attempt_ssh_users.each do |ssh_user_name|
85
- begin
86
- content = remote_file_contents(inst_addr, ssh_user_name, remote_path, opts)
87
- break
88
- rescue Net::SSH::AuthenticationFailed
89
- ui.warn "Authentication failed for user: #{ssh_user_name} on instance: #{inst_addr}"
75
+ def display_instance_failure(stack)
76
+ instances = stack.resources.all.find_all do |resource|
77
+ resource.state.to_s.end_with?('failed')
78
+ end.map do |resource|
79
+ # If compute instance, simply expand
80
+ if(resource.within?(:compute, :servers))
81
+ resource.instance
82
+ # If a waitcondition, check for instance ID
83
+ elsif(resource.type.to_s.downcase.end_with?('waitcondition'))
84
+ if(resource.status_reason.to_s.include?('uniqueId'))
85
+ srv_id = resource.status_reason.split(' ').last.strip
86
+ provider.connection.api_for(:compute).servers.get(srv_id)
87
+ end
90
88
  end
91
- end
92
- if(content)
93
- ui.info " content of #{remote_path}:"
94
- ui.info ""
95
- ui.info content
89
+ end.compact
90
+ if(instances.empty?)
91
+ ui.error 'Failed to locate any failed instances'
96
92
  else
97
- ui.error "Failed to retreive content from node at: #{inst_addr}"
93
+ log_path = Chef::Config[:knife][:cloudformation][:failure_log_path]
94
+ if(log_path.to_s.empty?)
95
+ log_path = '/var/log/chef/client.log'
96
+ end
97
+ opts = ssh_key ? {:keys => [ssh_key]} : {}
98
+ instances.each do |instance|
99
+ ui.info " -> Log inspect for #{instance.id}:"
100
+ address = instance.addresses_public.map do |address|
101
+ if(address.version == 4)
102
+ address.address
103
+ end
104
+ end
105
+ if(address)
106
+ ssh_attempt_users.each do |user|
107
+ begin
108
+ ui.info remote_file_contents(address.first, user, log_path, opts)
109
+ break
110
+ rescue Net::SSH::AuthenticationFailed
111
+ ui.warn "Authentication failed for user #{user} on instance #{address}"
112
+ rescue => e
113
+ ui.error "Failed to retrieve log: #{e}"
114
+ _debug e
115
+ break
116
+ end
117
+ end
118
+ end
119
+ end
98
120
  end
99
121
  end
100
122
 
101
- def attempt_ssh_users
102
- ([ssh_user] + Array(Chef::Config[:knife][:cloudformation][:ssh_attempt_users])).flatten.compact
103
- end
104
-
105
- def ssh_user
106
- Chef::Config[:knife][:cloudformation][:ssh_user] ||
123
+ # Users to attempt SSH connection
124
+ #
125
+ # @return [Array<String>] usernames for ssh connect attempt
126
+ def ssh_attempt_users
127
+ base_user = Chef::Config[:knife][:cloudformation][:ssh_user] ||
107
128
  Chef::Config[:knife][:ssh_user] ||
108
129
  ENV['USER']
130
+ [base_user, Chef::Config[:knife][:cloudformation][:ssh_attempt_users]].flatten.compact
109
131
  end
110
132
 
111
133
  def ssh_key
@@ -113,6 +135,72 @@ class Chef
113
135
  Chef::Config[:knife][:identity_file]
114
136
  end
115
137
 
138
+ def display_attribute(stack)
139
+ attr = config[:attribute].split('.').inject(stack) do |memo, key|
140
+ args = key.scan(/\(([^)]*)\)/).flatten.first.to_s
141
+ if(args)
142
+ args = args.split(',').map{|a| a.to_i.to_s == a ? a.to_i : a}
143
+ key = key.split('(').first
144
+ end
145
+ if(memo.public_methods.include?(key.to_sym))
146
+ if(args.size == 1 && args.first.to_s.start_with?('&'))
147
+ memo.send(key, &args.first.slice(2, args.first.size).to_sym)
148
+ else
149
+ memo.send(*[key, args].flatten.compact)
150
+ end
151
+ else
152
+ raise NoMethodError.new "Invalid attribute requested! (#{memo.class}##{key})"
153
+ end
154
+ end
155
+ ui.info " Attribute Lookup -> #{config[:attribute]}:"
156
+ ui.info MultiJson.dump(
157
+ MultiJson.load(
158
+ MultiJson.dump(attr)
159
+ ),
160
+ :pretty => true
161
+ )
162
+ end
163
+
164
+ def display_nodes(stack)
165
+ asg_nodes = Smash[
166
+ stack.resources.all.find_all do |resource|
167
+ resource.within?(:auto_scale, :groups)
168
+ end.map do |group_resource|
169
+ asg = group_resource.expand
170
+ [
171
+ asg.name,
172
+ Smash[
173
+ asg.servers.map(&:expand).map{|s|
174
+ [s.id, Smash.new(
175
+ :name => s.name,
176
+ :addresses => s.addresses.map(&:address)
177
+ )]
178
+ }
179
+ ]
180
+ ]
181
+ end
182
+ ]
183
+ compute_nodes = Smash[
184
+ stack.resources.all.find_all do |resource|
185
+ resource.within?(:compute, :servers)
186
+ end.map do |srv|
187
+ srv = srv.instance
188
+ [srv.id, Smash.new(
189
+ :name => srv.name,
190
+ :addresses => srv.addresses.map(&:address)
191
+ )]
192
+ end
193
+ ]
194
+ unless(asg_nodes.empty?)
195
+ ui.info ' AutoScale Group Instances:'
196
+ ui.info MultiJson.dump(asg_nodes, :pretty => true)
197
+ end
198
+ unless(compute_nodes.empty?)
199
+ ui.info ' Compute Instances:'
200
+ ui.info MultiJson.dump(compute_nodes, :pretty => true)
201
+ end
202
+ end
203
+
116
204
  end
117
205
  end
118
206
  end
@@ -1,9 +1,11 @@
1
- require 'knife-cloudformation/cloudformation_base'
1
+ require 'knife-cloudformation'
2
2
 
3
3
  class Chef
4
4
  class Knife
5
+ # Cloudformation list command
5
6
  class CloudformationList < Knife
6
- include KnifeCloudformation::KnifeBase
7
+
8
+ include KnifeCloudformation::Knife::Base
7
9
 
8
10
  banner 'knife cloudformation list NAME'
9
11
 
@@ -32,43 +34,37 @@ class Chef
32
34
  }
33
35
  )
34
36
 
35
- def run
37
+ # Run the list command
38
+ def _run
36
39
  things_output(nil, get_list, nil)
37
40
  end
38
41
 
42
+ # Get the list of stacks to display
43
+ #
44
+ # @return [Array<Hash>]
39
45
  def get_list
40
46
  get_things do
41
- aws.aws(:cloud_formation).list_stacks(list_options).body['StackSummaries'].sort do |x,y|
42
- if(y['CreationTime'].to_s.empty?)
47
+ provider.stacks.all.map do |stack|
48
+ Mash.new(stack.attributes)
49
+ end.sort do |x, y|
50
+ if(y[:created].to_s.empty?)
43
51
  -1
44
- elsif(x['CreationTime'].to_s.empty?)
52
+ elsif(x[:created].to_s.empty?)
45
53
  1
46
54
  else
47
- Time.parse(y['CreationTime'].to_s) <=> Time.parse(x['CreationTime'].to_s)
55
+ Time.parse(y['created'].to_s) <=> Time.parse(x['created'].to_s)
48
56
  end
49
57
  end
50
58
  end
51
59
  end
52
60
 
53
- def list_options
54
- status = Chef::Config[:knife][:cloudformation][:status] ||
55
- KnifeCloudformation::AwsCommons::DEFAULT_STACK_STATUS
56
- if(status.map(&:downcase).include?('none'))
57
- filter = {}
61
+ # @return [Array<String>] default attributes to display
62
+ def default_attributes
63
+ if(provider.connection.provider == :aws)
64
+ %w(name created status template_description)
58
65
  else
59
- count = 0
60
- filter = Hash[*(
61
- status.map do |n|
62
- count += 1
63
- ["StackStatusFilter.member.#{count}", n]
64
- end.flatten
65
- )]
66
+ %w(name created status description)
66
67
  end
67
- filter
68
- end
69
-
70
- def default_attributes
71
- %w(StackName CreationTime StackStatus TemplateDescription)
72
68
  end
73
69
 
74
70
  end