knife-cloudformation 0.1.22 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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