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
@@ -0,0 +1,28 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Helper methods for string format modification
7
+ module AnimalStrings
8
+
9
+ # Camel case string
10
+ #
11
+ # @param string [String]
12
+ # @return [String]
13
+ def camel(string)
14
+ string.to_s.split('_').map{|k| "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"}.join
15
+ end
16
+
17
+ # Snake case string
18
+ #
19
+ # @param string [String]
20
+ # @return [Symbol]
21
+ def snake(string)
22
+ string.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+ # Debug helpers
6
+ module Debug
7
+ # Output helpers
8
+ module Output
9
+ # Write debug message
10
+ #
11
+ # @param msg [String]
12
+ def debug(msg)
13
+ puts "<KnifeCloudformation>: #{msg}" if ENV['DEBUG']
14
+ end
15
+ end
16
+
17
+ class << self
18
+ # Load module into class
19
+ #
20
+ # @param klass [Class]
21
+ def included(klass)
22
+ klass.class_eval do
23
+ include Output
24
+ extend Output
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,64 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # JSON helper methods
7
+ module JSON
8
+
9
+ # Attempt to load chef JSON compat helper
10
+ #
11
+ # @return [TrueClass, FalseClass] chef compat helper available
12
+ def try_json_compat
13
+ unless(@_json_loaded)
14
+ begin
15
+ require 'chef/json_compat'
16
+ rescue
17
+ require "#{ENV['RUBY_JSON_LIB'] || 'json'}"
18
+ end
19
+ @_json_loaded = true
20
+ end
21
+ defined?(Chef::JSONCompat)
22
+ end
23
+
24
+ # Convert to JSON
25
+ #
26
+ # @param thing [Object]
27
+ # @return [String]
28
+ def _to_json(thing)
29
+ if(try_json_compat)
30
+ Chef::JSONCompat.to_json(thing)
31
+ else
32
+ JSON.dump(thing)
33
+ end
34
+ end
35
+
36
+ # Load JSON data
37
+ #
38
+ # @param thing [String]
39
+ # @return [Object]
40
+ def _from_json(thing)
41
+ if(try_json_compat)
42
+ Chef::JSONCompat.from_json(thing)
43
+ else
44
+ JSON.read(thing)
45
+ end
46
+ end
47
+
48
+ # Format object into pretty JSON
49
+ #
50
+ # @param thing [Object]
51
+ # @return [String]
52
+ def _format_json(thing)
53
+ thing = _from_json(thing) if thing.is_a?(String)
54
+ if(try_json_compat)
55
+ Chef::JSONCompat.to_json_pretty(thing)
56
+ else
57
+ JSON.pretty_generate(thing)
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Storage helpers
7
+ module ObjectStorage
8
+
9
+ # Write to file
10
+ #
11
+ # @param object [Object]
12
+ # @param path [String] path to write object
13
+ # @param directory [Miasma::Models::Storage::Directory]
14
+ # @return [String] file path
15
+ def file_store(object, path, directory)
16
+ raise NotImplementedError.new 'Internal updated required! :('
17
+ content = object.is_a?(String) ? object : Utils._format_json(object)
18
+ directory.files.create(
19
+ :identity => path,
20
+ :body => content
21
+ )
22
+ loc = directory.service.service.name.split('::').last.downcase
23
+ "#{loc}://#{directory.identity}/#{path}"
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,79 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+ # Output Helpers
6
+ module Output
7
+
8
+ # Process things and return items
9
+ #
10
+ # @param things [Array] items to process
11
+ # @param args [Hash] options
12
+ # @option args [TrueClass, FalseClass] :flat flatten result array
13
+ # @option args [Array] :attributes attributes to extract
14
+ # @todo this was extracted from events and needs to be cleaned up
15
+ def process(things, args={})
16
+ @event_ids ||= []
17
+ processed = things.reverse.map do |thing|
18
+ next if @event_ids.include?(thing['id'])
19
+ @event_ids.push(thing['id']).compact!
20
+ if(args[:attributes])
21
+ args[:attributes].map do |key|
22
+ thing[key].to_s
23
+ end
24
+ else
25
+ thing.values
26
+ end
27
+ end
28
+ args[:flat] ? processed.flatten : processed
29
+ end
30
+
31
+ # Generate formatted titles
32
+ #
33
+ # @param thing [Object] thing being processed
34
+ # @param args [Hash]
35
+ # @option args [Array] :attributes
36
+ # @return [Array<String>] formatted titles
37
+ def get_titles(thing, args={})
38
+ attrs = args[:attributes] || []
39
+ if(attrs.empty?)
40
+ hash = thing.is_a?(Array) ? thing.first : thing
41
+ hash ||= {}
42
+ attrs = hash.keys
43
+ end
44
+ titles = attrs.map do |key|
45
+ camel(key).gsub(/([a-z])([A-Z])/, '\1 \2')
46
+ end.compact
47
+ if(args[:format])
48
+ titles.map{|s| @ui.color(s, :bold)}
49
+ else
50
+ titles
51
+ end
52
+ end
53
+
54
+ # Output stack related things in nice format
55
+ #
56
+ # @param stack [String] name of stack
57
+ # @param things [Array] things to display
58
+ # @param what [String] description of things for output
59
+ # @param args [Symbol] options (:ignore_empty_output)
60
+ def things_output(stack, things, what, *args)
61
+ unless(args.include?(:no_title))
62
+ output = get_titles(things, :format => true, :attributes => allowed_attributes)
63
+ else
64
+ output = []
65
+ end
66
+ columns = allowed_attributes.size
67
+ output += process(things, :flat => true, :attributes => allowed_attributes)
68
+ output.compact!
69
+ if(output.empty?)
70
+ ui.warn 'No information found' unless args.include?(:ignore_empty_output)
71
+ else
72
+ ui.info "#{what.to_s.capitalize} for stack: #{ui.color(stack, :bold)}" if stack
73
+ ui.info "#{ui.list(output, :uneven_columns_across, columns)}"
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,99 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Helper methods for path selection
7
+ module PathSelector
8
+
9
+ # Humanize the base name of path
10
+ #
11
+ # @param path [String]
12
+ # @return [String]
13
+ def humanize_path_basename(path)
14
+ File.basename(path).sub(
15
+ File.extname(path), ''
16
+ ).split(/[-_]/).map(&:capitalize).join(' ')
17
+ end
18
+
19
+ # Prompt user for file selection
20
+ #
21
+ # @param directory [String] path to directory
22
+ # @param opts [Hash] options
23
+ # @option opts [Array<String>] :ignore_directories directory names
24
+ # @option opts [String] :directories_name title for directories
25
+ # @option opts [String] :files_name title for files
26
+ # @option opts [String] :filter_prefix only return results matching filter
27
+ # @return [String] file path
28
+ def prompt_for_file(directory, opts={})
29
+ file_list = Dir.glob(File.join(directory, '**', '**', '*')).find_all do |file|
30
+ File.file?(file)
31
+ end
32
+ if(opts[:filter_prefix])
33
+ file_list = file_list.find_all do |file|
34
+ file.start_with?(options[:filter_prefix])
35
+ end
36
+ end
37
+ directories = file_list.map do |file|
38
+ File.dirname(file)
39
+ end.uniq
40
+ files = file_list.find_all do |path|
41
+ path.sub(directory, '').split('/').size == 2
42
+ end
43
+ if(opts[:ignore_directories])
44
+ directories.delete_if do |dir|
45
+ opts[:ignore_directories].include?(File.basename(dir))
46
+ end
47
+ end
48
+ if(directories.empty? && files.empty?)
49
+ ui.fatal 'No formation paths discoverable!'
50
+ else
51
+ output = ['Please select an entry']
52
+ output << '(or directory to list):' unless directories.empty?
53
+ ui.info output.join(' ')
54
+ output.clear
55
+ idx = 1
56
+ valid = {}
57
+ unless(directories.empty?)
58
+ output << ui.color("#{opts.fetch(:directories_name, 'Directories')}:", :bold)
59
+ directories.each do |dir|
60
+ valid[idx] = {:path => dir, :type => :directory}
61
+ output << [idx, humanize_path_basename(dir)]
62
+ idx += 1
63
+ end
64
+ end
65
+ unless(files.empty?)
66
+ output << ui.color("#{opts.fetch(:files_name, 'Files')}:", :bold)
67
+ files.each do |file|
68
+ valid[idx] = {:path => file, :type => :file}
69
+ output << [idx, humanize_path_basename(file)]
70
+ idx += 1
71
+ end
72
+ end
73
+ max = idx.to_s.length
74
+ output.map! do |o|
75
+ if(o.is_a?(Array))
76
+ " #{o.first}.#{' ' * (max - o.first.to_s.length)} #{o.last}"
77
+ else
78
+ o
79
+ end
80
+ end
81
+ ui.info "#{output.join("\n")}\n"
82
+ response = ask_question('Enter selection: ').to_i
83
+ unless(valid[response])
84
+ ui.fatal 'How about using a real value'
85
+ exit 1
86
+ else
87
+ entry = valid[response.to_i]
88
+ if(entry[:type] == :directory)
89
+ prompt_for_file(entry[:path], opts)
90
+ else
91
+ "/#{entry[:path]}"
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,29 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ module Utils
5
+
6
+ # Helper methods for SSH interactions
7
+ module Ssher
8
+
9
+ # Retrieve file from remote node
10
+ #
11
+ # @param address [String]
12
+ # @param user [String]
13
+ # @param path [String] remote file path
14
+ # @param ssh_opts [Hash]
15
+ # @return [String, NilClass]
16
+ def remote_file_contents(address, user, path, ssh_opts={})
17
+ if(path.to_s.strip.empty?)
18
+ raise ArgumentError.new 'No file path provided!'
19
+ end
20
+ require 'net/ssh'
21
+ content = ''
22
+ ssh_session = Net::SSH.start(address, user, ssh_opts)
23
+ content = ssh_session.exec!("sudo cat #{path}")
24
+ content.empty? ? nil : content
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,271 @@
1
+ require 'chef'
2
+ require 'knife-cloudformation'
3
+
4
+ module KnifeCloudformation
5
+ module Utils
6
+
7
+ # Stack serialization helper
8
+ class StackExporter
9
+
10
+ include KnifeCloudformation::Utils::AnimalStrings
11
+ include KnifeCloudformation::Utils::JSON
12
+
13
+ # default chef environment name
14
+ DEFAULT_CHEF_ENVIRONMENT = '_default'
15
+ # default instance options
16
+ DEFAULT_OPTIONS = Mash.new(
17
+ :chef_popsicle => true,
18
+ :ignored_parameters => ['Environment', 'StackCreator', 'Creator'],
19
+ :chef_environment_parameter => 'Environment'
20
+ )
21
+ # default structure of export payload
22
+ DEFAULT_EXPORT_STRUCTURE = {
23
+ :stack => Mash.new(
24
+ :template => nil,
25
+ :options => {
26
+ :parameters => Mash.new,
27
+ :capabilities => [],
28
+ :notification_topics => []
29
+ }
30
+ ),
31
+ :generator => {
32
+ :timestamp => Time.now.to_i,
33
+ :name => 'knife-cloudformation',
34
+ :version => KnifeCloudformation::VERSION.version,
35
+ :provider => nil
36
+ }
37
+ }
38
+
39
+ # @return [Miasma::Models::Orchestration::Stack]
40
+ attr_reader :stack
41
+ # @return [Hash]
42
+ attr_reader :options
43
+ # @return [Hash]
44
+ attr_reader :stack_export
45
+
46
+ # Create new instance
47
+ #
48
+ # @param stack [Miasma::Models::Orchestration::Stack]
49
+ # @param options [Hash]
50
+ # @option options [KnifeCloudformation::Provider] :provider
51
+ # @option options [TrueClass, FalseClass] :chef_popsicle freeze run list
52
+ # @option options [Array<String>] :ignored_parameters
53
+ # @option options [String] :chef_environment_parameter
54
+ def initialize(stack, options={})
55
+ @stack = stack
56
+ @options = DEFAULT_OPTIONS.merge(options)
57
+ @stack_export = Mash.new
58
+ end
59
+
60
+ # Export stack
61
+ #
62
+ # @return [Hash] exported stack
63
+ def export
64
+ @stack_export = Mash.new(DEFAULT_EXPORT_STRUCTURE).tap do |stack_export|
65
+ [:parameters, :capabilities, :notification_topics].each do |key|
66
+ if(val = stack.send(key))
67
+ stack_export[:stack][key] = val
68
+ end
69
+ end
70
+ stack_export[:stack][:template] = stack.template
71
+ stack_export[:generator][:timestamp] = Time.now.to_i
72
+ stack_export[:generator][:provider] = stack.provider.connection.provider
73
+ if(chef_popsicle?)
74
+ freeze_runlists(stack_export)
75
+ end
76
+ remove_ignored_parameters(stack_export)
77
+ stack_export[:stack][:template] = _to_json(
78
+ stack_export[:stack][:template]
79
+ )
80
+ end
81
+ end
82
+
83
+ # Provide query methods on options hash
84
+ #
85
+ # @param args [Object] argument list
86
+ # @return [Object]
87
+ def method_missing(*args)
88
+ m = args.first.to_s
89
+ if(m.end_with?('?') && options.has_key?(k = m.sub('?', '').to_sym))
90
+ !!options[k]
91
+ else
92
+ super
93
+ end
94
+ end
95
+
96
+ protected
97
+
98
+ # Remove parameter values from export that are configured to be
99
+ # ignored
100
+ #
101
+ # @param export [Hash] stack export
102
+ # @return [Hash]
103
+ def remove_ignored_parameters(export)
104
+ options[:ignored_parameters].each do |param|
105
+ if(export[:stack][:options][:parameters])
106
+ export[:stack][:options][:parameters].delete(param)
107
+ end
108
+ end
109
+ export
110
+ end
111
+
112
+ # Environment name to use when interacting with Chef
113
+ #
114
+ # @param export [Hash] current export state
115
+ # @return [String] environment name
116
+ def chef_environment_name(export)
117
+ if(chef_environment_parameter?)
118
+ name = export[:stack][:options][:parameters][options[:chef_environment_parameter]]
119
+ end
120
+ name || DEFAULT_CHEF_ENVIRONMENT
121
+ end
122
+
123
+ # @return [Chef::Environment]
124
+ def environment
125
+ unless(@env)
126
+ @env = Chef::Environment.load('_default')
127
+ end
128
+ @env
129
+ end
130
+
131
+ # Find latest available cookbook version within
132
+ # the configured environment
133
+ #
134
+ # @param cookbook [String] name of cookbook
135
+ # @return [Chef::Version]
136
+ def allowed_cookbook_version(cookbook)
137
+ restriction = environment.cookbook_versions[cookbook]
138
+ requirement = Gem::Requirement.new(restriction)
139
+ Chef::CookbookVersion.available_versions(cookbook).detect do |v|
140
+ requirement.satisfied_by?(Gem::Version.new(v))
141
+ end
142
+ end
143
+
144
+ # Extract the runlist item. Fully expands roles and provides
145
+ # version pegged runlist.
146
+ #
147
+ # @param item [Chef::RunList::RunListItem, Array<String>]
148
+ # @return [Hash] new chef configuration hash
149
+ # @note this will expand all roles
150
+ def extract_runlist_item(item)
151
+ rl_item = item.is_a?(Chef::RunList::RunListItem) ? item : Chef::RunList::RunListItem.new(item)
152
+ static_content = Mash.new(:run_list => [])
153
+ if(rl_item.recipe?)
154
+ cookbook, recipe = rl_item.name.split('::')
155
+ peg_version = allowed_cookbook_version(cookbook)
156
+ static_content[:run_list] << "recipe[#{[cookbook, recipe || 'default'].join('::')}@#{peg_version}]"
157
+ elsif(rl_item.role?)
158
+ role = Chef::Role.load(rl_item.name)
159
+ role.run_list.each do |item|
160
+ static_content = Chef::Mixin::DeepMerge.merge(static_content, extract_runlist_item(item))
161
+ end
162
+ static_content = Chef::Mixin::DeepMerge.merge(
163
+ static_content, Chef::Mixin::DeepMerge.merge(role.default_attributes, role.override_attributes)
164
+ )
165
+ else
166
+ raise TypeError.new("Unknown chef run list item encountered: #{rl_item.inspect}")
167
+ end
168
+ static_content
169
+ end
170
+
171
+ # Expand any detected chef run lists and freeze them within the
172
+ # stack template
173
+ #
174
+ # @param first_run [Hash] chef first run hash
175
+ # @return [Hash]
176
+ def unpack_and_freeze_runlist(first_run)
177
+ extracted_runlists = first_run['run_list'].map do |item|
178
+ extract_runlist_item(cf_replace(item))
179
+ end
180
+ first_run.delete('run_list')
181
+ first_run.replace(
182
+ extracted_runlists.inject(first_run) do |memo, first_run_item|
183
+ Chef::Mixin::DeepMerge.merge(memo, first_run_item)
184
+ end
185
+ )
186
+ end
187
+
188
+ # Freeze chef run lists
189
+ #
190
+ # @param exported [Hash] stack export
191
+ # @return [Hash]
192
+ def freeze_runlists(exported)
193
+ first_runs = locate_runlists(exported)
194
+ first_runs.each do |first_run|
195
+ unpack_and_freeze_runlist(first_run)
196
+ end
197
+ exported
198
+ end
199
+
200
+ # Locate chef run lists within data collection
201
+ #
202
+ # @param thing [Enumerable] collection from export
203
+ # @return [Enumerable] updated collection from export
204
+ def locate_runlists(thing)
205
+ result = []
206
+ case thing
207
+ when Hash
208
+ if(thing['content'] && thing['content']['run_list'])
209
+ result << thing['content']
210
+ else
211
+ thing.each do |k,v|
212
+ result += locate_runlists(v)
213
+ end
214
+ end
215
+ when Array
216
+ thing.each do |v|
217
+ result += locate_runlists(v)
218
+ end
219
+ end
220
+ result
221
+ end
222
+
223
+ # Apply cloudformation function to data
224
+ #
225
+ # @param hsh [Object] stack template item
226
+ # @return [Object]
227
+ def cf_replace(hsh)
228
+ if(hsh.is_a?(Hash))
229
+ case hsh.keys.first
230
+ when 'Fn::Join'
231
+ cf_join(*hsh.values.first)
232
+ when 'Ref'
233
+ cf_ref(hsh.values.first)
234
+ else
235
+ hsh
236
+ end
237
+ else
238
+ hsh
239
+ end
240
+ end
241
+
242
+ # Apply Ref function
243
+ #
244
+ # @param ref_name [Hash]
245
+ # @return [Object] value in parameters
246
+ def cf_ref(ref_name)
247
+ if(stack.parameters.has_key?(ref_name))
248
+ stack.parameters[ref_name]
249
+ else
250
+ raise KeyError.new("No parameter found with given reference name (#{ref_name}). " <<
251
+ "Only parameter based references supported!")
252
+ end
253
+ end
254
+
255
+ # Apply Join function
256
+ #
257
+ # @param delim [String] join delimiter
258
+ # @param args [String, Hash] items to join
259
+ # @return [String]
260
+ def cf_join(delim, args)
261
+ args.map do |arg|
262
+ if(arg.is_a?(Hash))
263
+ cf_replace(arg)
264
+ else
265
+ arg.to_s
266
+ end
267
+ end.join(delim)
268
+ end
269
+ end
270
+ end
271
+ end