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