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
@@ -0,0 +1,8 @@
1
+ require 'knife-cloudformation'
2
+
3
+ module KnifeCloudformation
4
+ # Container for monkey patches
5
+ module MonkeyPatch
6
+ autoload :Stack, 'knife-cloudformation/monkey_patch/stack'
7
+ end
8
+ end
@@ -0,0 +1,195 @@
1
+ require 'base64'
2
+ require 'knife-cloudformation'
3
+
4
+ module KnifeCloudformation
5
+ module MonkeyPatch
6
+
7
+ # Expand stack model functionality
8
+ module Stack
9
+
10
+ include KnifeCloudformation::Utils::AnimalStrings
11
+
12
+ ## Status helpers
13
+
14
+ # Check for state suffix
15
+ #
16
+ # @param args [String, Symbol] state suffix to check for (multiple allowed)
17
+ # @return [TrueClass, FalseClass] true if any matches found in argument list
18
+ def status_ends_with?(*args)
19
+ stat = status.to_s.downcase
20
+ !!args.map(&:to_s).map(&:downcase).detect do |suffix|
21
+ stat.end_with?(suffix)
22
+ end
23
+ end
24
+
25
+ # Check for state prefix
26
+ #
27
+ # @param args [String, Symbol] state prefix to check for (multiple allowed)
28
+ # @return [TrueClass, FalseClass] true if any matches found in argument list
29
+ def status_starts_with?(*args)
30
+ stat = status.to_s.downcase
31
+ !!args.map(&:to_s).map(&:downcase).detect do |prefix|
32
+ stat.start_with?(prefix)
33
+ end
34
+ end
35
+
36
+ # Check for state inclusion
37
+ #
38
+ # @param args [String, Symbol] state string to check for (multiple allowed)
39
+ # @return [TrueClass, FalseClass] true if any matches found in argument list
40
+ def status_includes?(*args)
41
+ stat = status.to_s.downcase
42
+ !!args.map(&:to_s).map(&:downcase).detect do |string|
43
+ stat.include?(string)
44
+ end
45
+ end
46
+
47
+ # @return [TrueClass, FalseClass] stack is in progress
48
+ def in_progress?
49
+ status_ends_with?(:in_progress)
50
+ end
51
+
52
+ # @return [TrueClass, FalseClass] stack is in complete state
53
+ def complete?
54
+ status_ends_with?(:complete, :failed)
55
+ end
56
+
57
+ # @return [TrueClass, FalseClass] stack is failed state
58
+ def failed?
59
+ status_ends_with?(:failed) ||
60
+ (status_includes?(:rollback) && status_ends_with?(:complete))
61
+ end
62
+
63
+ # @return [TrueClass, FalseClass] stack is in success state
64
+ def success?
65
+ !failed? && complete?
66
+ end
67
+
68
+ # @return [TrueClass, FalseClass] stack is creating
69
+ def creating?
70
+ in_progress? && status_starts_with?(:create)
71
+ end
72
+
73
+ # @return [TrueClass, FalseClass] stack is deleting
74
+ def deleting?
75
+ in_progress? && status_starts_with?(:delete)
76
+ end
77
+
78
+ # @return [TrueClass, FalseClass] stack is updating
79
+ def updating?
80
+ in_progress? && status_starts_with?(:update)
81
+ end
82
+
83
+ # @return [TrueClass, FalseClass] stack is rolling back
84
+ def rollbacking?
85
+ in_progress? && status_starts_with?(:rollback)
86
+ end
87
+
88
+ # @return [String] action currently being performed
89
+ def performing
90
+ if(in_progress?)
91
+ status.to_s.downcase.split('_').first.to_sym
92
+ end
93
+ end
94
+
95
+ ### Color coders
96
+
97
+ # @return [TrueClass, FalseClass] stack is in red state
98
+ def red?
99
+ failed? || deleting?
100
+ end
101
+
102
+ # @return [TrueClass, FalseClass] stack is in green state
103
+ def green?
104
+ success?
105
+ end
106
+
107
+ # @return [TrueClass, FalseClass] stack is in yellow state
108
+ def yellow?
109
+ !red? && !green?
110
+ end
111
+
112
+ # Provides color of stack state. Red is an error state, yellow
113
+ # is a warning state and green is a success state
114
+ #
115
+ # @return [Symbol] color of state (:red, :yellow, :green)
116
+ def color_state
117
+ red? ? :red : green? ? :green : :yellow
118
+ end
119
+
120
+ # Provides text of stack state. Danger is an error state, warning
121
+ # is a warning state and success is a success state
122
+ #
123
+ # @return [Symbol] color of state (:danger, :warning, :success)
124
+ def text_state
125
+ red? ? :danger : green? ? :success : :warning
126
+ end
127
+
128
+ # @return [String] URL safe encoded stack id
129
+ def encoded_id
130
+ Base64.urlsafe_encode64(id)
131
+ end
132
+
133
+ # Whole number representation of current completion
134
+ #
135
+ # @param min [Integer] lowest allowed return value (defaults 5)
136
+ # @return [Integer] percent complete (0..100)
137
+ def percent_complete(min = 5)
138
+ if(in_progress?)
139
+ total_resources = load_template.fetch('Resources', []).size
140
+ total_complete = resources.all.find_all do |resource|
141
+ resource.resource_status.downcase.end_with?('complete')
142
+ end.size
143
+ result = ((total_complete.to_f / total_resources) * 100).to_i
144
+ result > min.to_i ? result : min
145
+ else
146
+ 100
147
+ end
148
+ end
149
+
150
+ # Apply stack outputs to current stack parameters
151
+ #
152
+ # @param remote_stack [Miasma::Orchestration::Stack]
153
+ # @return [self]
154
+ # @note setting `DisableApply` within parameter hash will
155
+ # prevent parameters being overridden
156
+ def apply_stack(remote_stack)
157
+ default_key = 'Default'
158
+ stack_parameters = template.fetch('Parameters',
159
+ template.fetch('parameters', {})
160
+ )
161
+ valid_parameters = Hash[
162
+ stack_parameters.map do |key, val|
163
+ unless(val['DisableApply'])
164
+ [snake(key), key]
165
+ end
166
+ end.compact
167
+ ]
168
+ if(defined?(Chef::Config) && Chef::Config[:knife][:cloudformation][:ignore_parameters])
169
+ valid_parameters = valid_parameters.map do |snake_param, camel_param|
170
+ unless(Chef::Config[:knife][:cloudformation][:ignore_parameters].include?(camel_param))
171
+ [snake_param, camel_param]
172
+ end
173
+ end.compact
174
+ end
175
+ if(persisted?)
176
+ remote_stack.outputs.each do |output|
177
+ if(param_key = valid_parameters[snake(output.key)])
178
+ parameters.merge!(param_key => output.value)
179
+ end
180
+ end
181
+ else
182
+ remote_stack.outputs.each do |output|
183
+ if(param_key = valid_parameters[snake(output.key)])
184
+ stack_parameters[param_key][default_key] = output.value
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ end
191
+ end
192
+ end
193
+
194
+ # Infect miasma
195
+ Miasma::Models::Orchestration::Stack.send(:include, KnifeCloudformation::MonkeyPatch::Stack)
@@ -0,0 +1,225 @@
1
+ require 'chef/mash'
2
+ require 'logger'
3
+ require 'chef/mixin/deep_merge'
4
+ require 'knife-cloudformation'
5
+
6
+ module KnifeCloudformation
7
+ # Remote provider interface
8
+ class Provider
9
+
10
+ include KnifeCloudformation::Utils::AnimalStrings
11
+
12
+ # Minimum number of seconds to wait before re-expanding in
13
+ # progress stack
14
+ STACK_EXPAND_INTERVAL = 45
15
+
16
+ # Default interval for refreshing stack list in cache
17
+ STACK_LIST_INTERVAL = 120
18
+
19
+ # @return [Miasma::Models::Orchestration]
20
+ attr_reader :connection
21
+ # @return [Cache]
22
+ attr_reader :cache
23
+ # @return [Thread, NilClass] stack list updater
24
+ attr_accessor :updater
25
+ # @return [TrueClass, FalseClass] async updates
26
+ attr_reader :async
27
+ # @return [Logger, NilClass] logger in use
28
+ attr_reader :logger
29
+ # @return [Numeric] interval between stack expansions
30
+ attr_reader :stack_expansion_interval
31
+ # @return [Numeric] interval between stack list updates
32
+ attr_reader :stack_list_interval
33
+
34
+ # Create new instance
35
+ #
36
+ # @param args [Hash]
37
+ # @option args [Hash] :miasma miasma connection hash
38
+ # @option args [Cache] :cache
39
+ # @option args [TrueClass, FalseClass] :async fetch stacks async (defaults true)
40
+ # @option args [Logger] :logger use custom logger
41
+ # @option args [Numeric] :stack_expansion_interval interval to wait between stack data expands
42
+ # @option args [Numeric] :stack_list_interval interval to wait between stack list refresh
43
+ def initialize(args={})
44
+ unless(args[:miasma][:provider])
45
+ best_guess = args[:miasma].keys.group_by do |key|
46
+ key.to_s.split('_').first
47
+ end.sort do |x, y|
48
+ y.size <=> x.size
49
+ end.first
50
+ if(best_guess)
51
+ provider = best_guess.first.to_sym
52
+ else
53
+ raise ArgumentError.new 'Cannot auto determine :provider value for credentials'
54
+ end
55
+ else
56
+ provider = args[:miasma].delete(:provider).to_sym
57
+ end
58
+ if(provider == :aws)
59
+ if(args[:miasma][:region])
60
+ args[:miasma][:aws_region] = args[:miasma].delete(:region)
61
+ end
62
+ end
63
+ if(ENV['DEBUG'].to_s.downcase == 'true')
64
+ log_to = STDOUT
65
+ else
66
+ if(Gem.win_platform?)
67
+ log_to = 'NUL'
68
+ else
69
+ log_to = '/dev/null'
70
+ end
71
+ end
72
+ @logger = args.fetch(:logger, Logger.new(log_to))
73
+ @stack_expansion_interval = args.fetch(:stack_expansion_interval, STACK_EXPAND_INTERVAL)
74
+ @stack_list_interval = args.fetch(:stack_list_interval, STACK_LIST_INTERVAL)
75
+ @connection = Miasma.api(
76
+ :provider => provider,
77
+ :type => :orchestration,
78
+ :credentials => args[:miasma]
79
+ )
80
+ @cache = args.fetch(:cache, Cache.new(:local))
81
+ @async = args.fetch(:async, true)
82
+ @miamsa_args = args[:miasma].dup
83
+ cache.init(:stacks_lock, :lock, :timeout => 0.1)
84
+ cache.init(:stacks, :stamped)
85
+ cache.init(:stack_expansion_lock, :lock, :timeout => 0.1)
86
+ if(args.fetch(:fetch, false))
87
+ async ? update_stack_list! : fetch_stacks
88
+ end
89
+ end
90
+
91
+ # @return [Miasma::Orchestration::Stacks]
92
+ def stacks
93
+ connection.stacks.from_json(cached_stacks)
94
+ end
95
+
96
+ # @return [String] json representation of cached stacks
97
+ def cached_stacks
98
+ fetch_stacks unless @initial_fetch_complete
99
+ value = cache[:stacks].value
100
+ value ? MultiJson.dump(MultiJson.load(value).values) : '[]'
101
+ end
102
+
103
+ # @return [Miasma::Orchestration::Stack, NilClass]
104
+ def stack(stack_id)
105
+ stacks.get(stack_id)
106
+ end
107
+
108
+ # Store stack attribute changes
109
+ #
110
+ # @param stack_id [String]
111
+ # @param stack_attributes [Hash]
112
+ # @return [TrueClass]
113
+ def save_expanded_stack(stack_id, stack_attributes)
114
+ current_stacks = MultiJson.load(cached_stacks)
115
+ cache.locked_action(:stacks_lock) do
116
+ logger.info "Saving expanded stack attributes in cache (#{stack_id})"
117
+ current_stacks[stack_id] = stack_attributes.merge('Cached' => Time.now.to_i)
118
+ cache[:stacks].value = MultiJson.dump(current_stacks)
119
+ end
120
+ true
121
+ end
122
+
123
+ # Remove stack from the cache
124
+ #
125
+ # @param stack_id [String]
126
+ # @return [TrueClass, FalseClass]
127
+ def remove_stack(stack_id)
128
+ current_stacks = MultiJson.load(cached_stacks)
129
+ logger.info "Attempting to remove stack from internal cache (#{stack_id})"
130
+ cache.locked_action(:stacks_lock) do
131
+ val = current_stacks.delete(stack_id)
132
+ logger.info "Successfully removed stack from internal cache (#{stack_id})"
133
+ cache[:stacks].value = MultiJson.dump(current_stacks)
134
+ !!val
135
+ end
136
+ end
137
+
138
+ # Expand all lazy loaded attributes within stack
139
+ #
140
+ # @param stack [Miasma::Models::Orchestration::Stack]
141
+ def expand_stack(stack)
142
+ logger.info "Stack expansion requested (#{stack.id})"
143
+ if((stack.in_progress? && Time.now.to_i - stack.attributes['Cached'].to_i > stack_expansion_interval) ||
144
+ !stack.attributes['Cached'])
145
+ begin
146
+ expanded = false
147
+ cache.locked_action(:stack_expansion_lock) do
148
+ expanded = true
149
+ stack.reload
150
+ stack.data['Cached'] = Time.now.to_i
151
+ end
152
+ if(expanded)
153
+ save_expanded_stack(stack.id, stack.to_json)
154
+ end
155
+ rescue => e
156
+ logger.error "Stack expansion failed (#{stack.id}) - #{e.class}: #{e}"
157
+ end
158
+ else
159
+ logger.info "Stack has been cached within expand interval. Expansion prevented. (#{stack.id})"
160
+ end
161
+ end
162
+
163
+ # Request stack information and store in cache
164
+ #
165
+ # @return [TrueClass]
166
+ def fetch_stacks
167
+ cache.locked_action(:stacks_lock) do
168
+ logger.info "Lock aquired for stack update. Requesting stacks from upstream. (#{Thread.current})"
169
+ stacks = Hash[
170
+ connection.stacks.reload.all.map do |stack|
171
+ [stack.id, stack.attributes]
172
+ end
173
+ ]
174
+ if(cache[:stacks].value)
175
+ existing_stacks = MultiJson.load(cache[:stacks].value)
176
+ # Force common types
177
+ stacks = MultiJson.load(MultiJson.dump(stacks))
178
+ # Remove stacks that have been deleted
179
+ stale_ids = existing_stacks.keys - stacks.keys
180
+ stacks = Chef::Mixin::DeepMerge.merge(existing_stacks, stacks)
181
+ stale_ids.each do |stale_id|
182
+ stacks.delete(stale_id)
183
+ end
184
+ end
185
+ cache[:stacks].value = stacks.to_json
186
+ logger.info 'Stack list has been updated from upstream and cached locally'
187
+ end
188
+ @initial_fetch_complete = true
189
+ end
190
+
191
+ # Start async stack list update. Creates thread that loops every
192
+ # `self.stack_list_interval` seconds and refreshes stack list in cache
193
+ #
194
+ # @return [TrueClass, FalseClass]
195
+ def update_stack_list!
196
+ if(updater.nil? || !updater.alive?)
197
+ self.updater = Thread.new{
198
+ loop do
199
+ begin
200
+ fetch_stacks
201
+ sleep(stack_list_interval)
202
+ rescue => e
203
+ logger.error "Failure encountered on stack fetch: #{e.class} - #{e}"
204
+ end
205
+ end
206
+ }
207
+ true
208
+ else
209
+ false
210
+ end
211
+ end
212
+
213
+ # Build API connection for service type
214
+ #
215
+ # @param service [String, Symbol]
216
+ # @return [Miasma::Model]
217
+ def service_for(service)
218
+ connection.api_for(service)
219
+ end
220
+
221
+ end
222
+ end
223
+
224
+ # Release the monkeys!
225
+ KnifeCloudformation::MonkeyPatch::Stack
@@ -1,104 +1,24 @@
1
+ require 'knife-cloudformation'
2
+
1
3
  module KnifeCloudformation
4
+ # Utility classes and modules
2
5
  module Utils
3
6
 
4
- module Debug
5
- module Output
6
- def debug(msg)
7
- puts "<KnifeCloudformation>: #{msg}" if ENV['DEBUG']
8
- end
9
- end
10
-
11
- class << self
12
- def included(klass)
13
- klass.class_eval do
14
- include Output
15
- extend Output
16
- end
17
- end
18
- end
19
- end
20
-
21
- module JSON
22
-
23
- def try_json_compat
24
- unless(@_json_loaded)
25
- begin
26
- require 'chef/json_compat'
27
- rescue
28
- require "#{ENV['RUBY_JSON_LIB'] || 'json'}"
29
- end
30
- @_json_loaded = true
31
- end
32
- defined?(Chef::JSONCompat)
33
- end
34
-
35
- def _to_json(thing)
36
- if(try_json_compat)
37
- Chef::JSONCompat.to_json(thing)
38
- else
39
- JSON.dump(thing)
40
- end
41
- end
42
-
43
- def _from_json(thing)
44
- if(try_json_compat)
45
- Chef::JSONCompat.from_json(thing)
46
- else
47
- JSON.read(thing)
48
- end
49
- end
50
-
51
- def _format_json(thing)
52
- thing = _from_json(thing) if thing.is_a?(String)
53
- if(try_json_compat)
54
- Chef::JSONCompat.to_json_pretty(thing)
55
- else
56
- JSON.pretty_generate(thing)
57
- end
58
- end
59
-
60
- end
61
-
62
- module AnimalStrings
63
-
64
- def camel(string)
65
- string.to_s.split('_').map{|k| "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"}.join
66
- end
67
-
68
- def snake(string)
69
- string.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
70
- end
71
-
72
- end
7
+ autoload :Output, 'knife-cloudformation/utils/output'
8
+ autoload :StackParameterValidator, 'knife-cloudformation/utils/stack_parameter_validator'
9
+ autoload :StackParameterScrubber, 'knife-cloudformation/utils/stack_parameter_scrubber'
10
+ autoload :StackExporter, 'knife-cloudformation/utils/stack_exporter'
11
+ autoload :Debug, 'knife-cloudformation/utils/debug'
12
+ autoload :JSON, 'knife-cloudformation/utils/json'
13
+ autoload :AnimalStrings, 'knife-cloudformation/utils/animal_strings'
14
+ autoload :Ssher, 'knife-cloudformation/utils/ssher'
15
+ autoload :ObjectStorage, 'knife-cloudformation/utils/object_storage'
16
+ autoload :PathSelector, 'knife-cloudformation/utils/path_selector'
17
+
18
+ # Provide methods directly from module for previous version compatibility
19
+ extend JSON
20
+ extend AnimalStrings
21
+ extend ObjectStorage
73
22
 
74
- module Ssher
75
- def remote_file_contents(address, user, path, ssh_opts={})
76
- require 'net/sftp'
77
- content = ''
78
- ssh_session = Net::SSH.start(address, user, ssh_opts)
79
- con = Net::SFTP::Session.new(ssh_session)
80
- con.loop{ con.opening? }
81
- f_handle = con.open!(path)
82
- data = ''
83
- count = 0
84
- while(data)
85
- data = nil
86
- request = con.read(f_handle, count, 1024) do |response|
87
- unless(response.eof?)
88
- if(response.ok?)
89
- count += 1024
90
- content << response[:data]
91
- data = true
92
- end
93
- end
94
- end
95
- request.wait
96
- end
97
- con.close!(f_handle)
98
- con.close_channel
99
- ssh_session.close
100
- content.empty? ? nil : content
101
- end
102
- end
103
23
  end
104
24
  end