sfn 0.0.1 → 0.3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -0
  3. data/LICENSE +13 -0
  4. data/README.md +142 -61
  5. data/bin/sfn +43 -0
  6. data/lib/chef/knife/knife_plugin_seed.rb +117 -0
  7. data/lib/sfn.rb +17 -0
  8. data/lib/sfn/cache.rb +385 -0
  9. data/lib/sfn/command.rb +45 -0
  10. data/lib/sfn/command/create.rb +87 -0
  11. data/lib/sfn/command/describe.rb +87 -0
  12. data/lib/sfn/command/destroy.rb +74 -0
  13. data/lib/sfn/command/events.rb +98 -0
  14. data/lib/sfn/command/export.rb +103 -0
  15. data/lib/sfn/command/import.rb +117 -0
  16. data/lib/sfn/command/inspect.rb +160 -0
  17. data/lib/sfn/command/list.rb +59 -0
  18. data/lib/sfn/command/promote.rb +17 -0
  19. data/lib/sfn/command/update.rb +95 -0
  20. data/lib/sfn/command/validate.rb +34 -0
  21. data/lib/sfn/command_module.rb +9 -0
  22. data/lib/sfn/command_module/base.rb +150 -0
  23. data/lib/sfn/command_module/stack.rb +166 -0
  24. data/lib/sfn/command_module/template.rb +147 -0
  25. data/lib/sfn/config.rb +106 -0
  26. data/lib/sfn/config/create.rb +35 -0
  27. data/lib/sfn/config/describe.rb +19 -0
  28. data/lib/sfn/config/destroy.rb +9 -0
  29. data/lib/sfn/config/events.rb +25 -0
  30. data/lib/sfn/config/export.rb +29 -0
  31. data/lib/sfn/config/import.rb +24 -0
  32. data/lib/sfn/config/inspect.rb +37 -0
  33. data/lib/sfn/config/list.rb +25 -0
  34. data/lib/sfn/config/promote.rb +23 -0
  35. data/lib/sfn/config/update.rb +20 -0
  36. data/lib/sfn/config/validate.rb +49 -0
  37. data/lib/sfn/monkey_patch.rb +8 -0
  38. data/lib/sfn/monkey_patch/stack.rb +200 -0
  39. data/lib/sfn/provider.rb +224 -0
  40. data/lib/sfn/utils.rb +23 -0
  41. data/lib/sfn/utils/debug.rb +31 -0
  42. data/lib/sfn/utils/json.rb +37 -0
  43. data/lib/sfn/utils/object_storage.rb +28 -0
  44. data/lib/sfn/utils/output.rb +79 -0
  45. data/lib/sfn/utils/path_selector.rb +99 -0
  46. data/lib/sfn/utils/ssher.rb +29 -0
  47. data/lib/sfn/utils/stack_exporter.rb +275 -0
  48. data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
  49. data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
  50. data/lib/sfn/version.rb +4 -0
  51. data/sfn.gemspec +19 -0
  52. metadata +110 -4
data/bin/sfn ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bogo-cli'
4
+ require 'sfn'
5
+
6
+ Bogo::Cli::Setup.define do
7
+
8
+ on :v, :version, 'Print version ' do
9
+ puts "sfn - SparkleFormation CLI - [Version: #{Sfn::VERSION}]"
10
+ exit
11
+ end
12
+
13
+ Sfn::Config.constants.map do |konst|
14
+ const = Sfn::Config.const_get(konst)
15
+ if(const.is_a?(Class) && const.ancestors.include?(Bogo::Config))
16
+ const
17
+ end
18
+ end.compact.sort_by(&:to_s).each do |klass|
19
+
20
+ klass_name = klass.name.split('::').last.downcase
21
+
22
+ command klass_name do
23
+ if(klass.const_defined?(:DESCRIPTION))
24
+ description klass.const_get(:DESCRIPTION)
25
+ end
26
+
27
+ Sfn::Config.options_for(klass).each do |name, info|
28
+ on_name = info[:boolean] ? info[:long] : "#{info[:long]}="
29
+ opts = Smash.new.tap do |o|
30
+ o[:default] = info[:default] if info[:default]
31
+ o[:as] = Array if info[:multiple]
32
+ end
33
+ on info[:short], on_name, info[:description], :default => info[:default]
34
+ end
35
+
36
+ run do |opts, args|
37
+ Bogo::Utility.constantize(klass.to_s.sub('Config', 'Command')).new(opts, args).execute!
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,117 @@
1
+ begin
2
+ kcfn = Gem::Specification.find_by_name('knife-cloudformation')
3
+ $stderr.puts "[WARN]: Deprecated gem detected: #{kcfn.name} [V: #{kcfn.version}]"
4
+ $stderr.puts '[WARN]: Uninstall gem to prevent any conflicts (`gem uninstall knife-cloudformation -a`)'
5
+ rescue Gem::LoadError => e
6
+ # ignore
7
+ end
8
+
9
+ unless(defined?(Chef::Knife::CloudformationCreate))
10
+
11
+ require 'sfn'
12
+ require 'bogo'
13
+
14
+ Chef::Config[:knife][:cloudformation] = {
15
+ :options => {},
16
+ :create => {},
17
+ :update => {}
18
+ }
19
+ Chef::Config[:knife][:sparkleformation] = Chef::Config[:knife][:cloudformation]
20
+
21
+ VALID_PREFIX = ['cloudformation', 'sparkleformation']
22
+
23
+
24
+ Sfn::Config.constants.map do |konst|
25
+ const = Sfn::Config.const_get(konst)
26
+ if(const.is_a?(Class) && const.ancestors.include?(Bogo::Config))
27
+ const
28
+ end
29
+ end.compact.sort_by(&:to_s).each do |klass|
30
+
31
+ VALID_PREFIX.each do |prefix|
32
+
33
+ klass_name = klass.name.split('::').last
34
+ command_class = "#{prefix.capitalize}#{klass_name}"
35
+
36
+ knife_klass = Class.new(Chef::Knife)
37
+ knife_klass.class_eval do
38
+
39
+ include Bogo::AnimalStrings
40
+
41
+ # Stub in names so knife will detect
42
+ def self.name
43
+ @name
44
+ end
45
+
46
+ def self.sfn_class
47
+ @sfn_class
48
+ end
49
+
50
+ def name
51
+ self.class.name
52
+ end
53
+
54
+ # Properly load in configurations and execute command
55
+ def run
56
+ knife = Chef::Config[:knife]
57
+ if(knife.respond_to?(:hash_dup))
58
+ knife = knife.hash_dup
59
+ end
60
+ base = knife.to_smash
61
+ keys = VALID_PREFIX.dup
62
+ cmd_config = keys.unshift(keys.delete(snake(self.class.name.split('::').last).to_s.split('_').first)).map do |k|
63
+ base[k]
64
+ end.compact.first || {}
65
+ cmd_config = cmd_config.to_smash
66
+ reconfig = config.find_all do |k,v|
67
+ !v.nil?
68
+ end
69
+ # Split up options provided multiple arguments
70
+ reconfig.map! do |k,v|
71
+ if(v.is_a?(String) && v.include?(','))
72
+ v = v.split(',').map(&:strip)
73
+ end
74
+ [k,v]
75
+ end
76
+ config = Smash[reconfig]
77
+ cmd_config = cmd_config.deep_merge(config)
78
+ self.class.sfn_class.new(cmd_config, name_args).execute!
79
+ end
80
+
81
+ end
82
+ knife_klass.instance_variable_set(:@name, "Chef::Knife::#{command_class}")
83
+ knife_klass.instance_variable_set(
84
+ :@sfn_class,
85
+ Bogo::Utility.constantize(klass.name.sub('Config', 'Command'))
86
+ )
87
+ knife_klass.banner "knife #{prefix} #{Bogo::Utility.snake(klass_name)}"
88
+
89
+ Sfn::Config.options_for(klass).each do |name, info|
90
+ if(info[:boolean])
91
+ short = "-#{info[:short]}"
92
+ long = "--[no-]#{info[:long]}"
93
+ else
94
+ val = 'VALUE'
95
+ if(info[:multiple])
96
+ val << '[,VALUE]'
97
+ end
98
+ short = "-#{info[:short]} #{val}"
99
+ long = "--#{info[:long]} #{val}"
100
+ end
101
+ knife_klass.option(
102
+ name.to_sym, {
103
+ :short => short,
104
+ :long => long,
105
+ :boolean => info[:boolean],
106
+ :default => info[:default],
107
+ :description => info[:description]
108
+ }
109
+ )
110
+ end
111
+ # Set the class as a proper constant
112
+ Chef::Knife.const_set(command_class, knife_klass)
113
+ # Force knife to pick up as a subcommand
114
+ Chef::Knife.inherited(knife_klass)
115
+ end
116
+ end
117
+ end
data/lib/sfn.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'sfn/version'
2
+ require 'miasma'
3
+ require 'bogo'
4
+
5
+ module Sfn
6
+
7
+ autoload :Provider, 'sfn/provider'
8
+ autoload :Cache, 'sfn/cache'
9
+ autoload :Config, 'sfn/config'
10
+ autoload :Export, 'sfn/export'
11
+ autoload :Utils, 'sfn/utils'
12
+ autoload :MonkeyPatch, 'sfn/monkey_patch'
13
+ autoload :Knife, 'sfn/knife'
14
+ autoload :Command, 'sfn/command'
15
+ autoload :CommandModule, 'sfn/command_module'
16
+
17
+ end
data/lib/sfn/cache.rb ADDED
@@ -0,0 +1,385 @@
1
+ require 'digest/sha2'
2
+ require 'thread'
3
+ require 'sfn'
4
+
5
+ module Sfn
6
+ # Data caching helper
7
+ class Cache
8
+
9
+ class << self
10
+
11
+ # Configure the caching approach to use
12
+ #
13
+ # @param type [Symbol] :redis or :local
14
+ # @param args [Hash] redis connection arguments if used
15
+ def configure(type, args={})
16
+ type = type.to_sym
17
+ case type
18
+ when :redis
19
+ begin
20
+ require 'redis-objects'
21
+ rescue LoadError
22
+ $stderr.puts 'The `redis-objects` gem is required for Cache support!'
23
+ raise
24
+ end
25
+ @_pid = Process.pid
26
+ Redis::Objects.redis = Redis.new(args)
27
+ when :local
28
+ else
29
+ raise TypeError.new("Unsupported caching type: #{type}")
30
+ end
31
+ enable(type)
32
+ end
33
+
34
+ # Set enabled caching type
35
+ #
36
+ # @param type [Symbol]
37
+ # @return [Symbol]
38
+ def enable(type)
39
+ @type = type.to_sym
40
+ end
41
+
42
+ # @return [Symbol] type of caching enabled
43
+ def type
44
+ @type || :local
45
+ end
46
+
47
+ # Set/get time limit on data type
48
+ #
49
+ # @param kind [String, Symbol] data type
50
+ # @param seconds [Integer]
51
+ # return [Integer] seconds
52
+ def apply_limit(kind, seconds=nil)
53
+ @apply_limit ||= {}
54
+ if(seconds)
55
+ @apply_limit[kind.to_sym] = seconds.to_i
56
+ end
57
+ @apply_limit[kind.to_sym].to_i
58
+ end
59
+
60
+ # @return [Hash] default limits
61
+ def default_limits
62
+ (@apply_limit || {}).dup
63
+ end
64
+
65
+ # Ping the redis connection and reconnect if dead
66
+ def redis_ping!
67
+ if((@_pid && @_pid != Process.pid) || !Redis::Objects.redis.connected?)
68
+ Redis::Objects.redis.client.reconnect
69
+ @_pid = Process.pid
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ # @return [String] custom key for this cache
76
+ attr_reader :key
77
+
78
+ # Create new instance
79
+ #
80
+ # @param key [String, Array]
81
+ def initialize(key)
82
+ if(key.respond_to?(:sort))
83
+ key = key.flatten if key.respond_to?(:flatten)
84
+ key = key.map(&:to_s).sort
85
+ end
86
+ @key = Digest::SHA256.hexdigest(key.to_s)
87
+ @apply_limit = self.class.default_limits
88
+ end
89
+
90
+ # Initialize a new data type
91
+ #
92
+ # @param name [Symbol] name of data
93
+ # @param kind [Symbol] data type
94
+ # @param args [Hash] options for data type
95
+ def init(name, kind, args={})
96
+ get_storage(self.class.type, kind, name, args)
97
+ true
98
+ end
99
+
100
+ # @return [Hash] data registry
101
+ def registry
102
+ get_storage(self.class.type, :hash, "registry_#{key}")
103
+ end
104
+
105
+ # Clear data
106
+ #
107
+ # @param args [Symbol] list of names to delete
108
+ # @return [TrueClass]
109
+ # @note clears all data if no names provided
110
+ def clear!(*args)
111
+ internal_lock do
112
+ args = registry.keys if args.empty?
113
+ args.each do |key|
114
+ value = self[key]
115
+ if(value.respond_to?(:clear))
116
+ value.clear
117
+ elsif(value.respond_to?(:value))
118
+ value.value = nil
119
+ end
120
+ registry.delete(key)
121
+ end
122
+ yield if block_given?
123
+ end
124
+ true
125
+ end
126
+
127
+ # Fetch item from storage
128
+ #
129
+ # @param store_type [Symbol]
130
+ # @param data_type [Symbol]
131
+ # @param name [Symbol] name of data
132
+ # @param args [Hash] options for underlying storage
133
+ # @return [Object]
134
+ def get_storage(store_type, data_type, name, args={})
135
+ full_name = "#{key}_#{name}"
136
+ result = nil
137
+ case store_type.to_sym
138
+ when :redis
139
+ result = get_redis_storage(data_type, full_name.to_s, args)
140
+ when :local
141
+ @_local_cache ||= {}
142
+ unless(@_local_cache[full_name.to_s])
143
+ @_local_cache[full_name.to_s] = get_local_storage(data_type, full_name.to_s, args)
144
+ end
145
+ result = @_local_cache[full_name.to_s]
146
+ else
147
+ raise TypeError.new("Unsupported caching storage type encountered: #{store_type}")
148
+ end
149
+ unless(full_name == "#{key}_registry_#{key}")
150
+ registry[name.to_s] = data_type
151
+ end
152
+ result
153
+ end
154
+
155
+ # Fetch item from redis storage
156
+ #
157
+ # @param data_type [Symbol]
158
+ # @param full_name [Symbol]
159
+ # @param args [Hash]
160
+ # @return [Object]
161
+ def get_redis_storage(data_type, full_name, args={})
162
+ self.class.redis_ping!
163
+ case data_type.to_sym
164
+ when :array
165
+ Redis::List.new(full_name, {:marshal => true}.merge(args))
166
+ when :hash
167
+ Redis::HashKey.new(full_name)
168
+ when :value
169
+ Redis::Value.new(full_name, {:marshal => true}.merge(args))
170
+ when :lock
171
+ Redis::Lock.new(full_name, {:expiration => 60, :timeout => 0.1}.merge(args))
172
+ when :stamped
173
+ Stamped.new(full_name.sub("#{key}_", '').to_sym, get_redis_storage(:value, full_name), self)
174
+ else
175
+ raise TypeError.new("Unsupported caching data type encountered: #{data_type}")
176
+ end
177
+ end
178
+
179
+ # Fetch item from local storage
180
+ #
181
+ # @param data_type [Symbol]
182
+ # @param full_name [Symbol]
183
+ # @param args [Hash]
184
+ # @return [Object]
185
+ # @todo make proper singleton for local storage
186
+ def get_local_storage(data_type, full_name, args={})
187
+ @storage ||= {}
188
+ @storage[full_name] ||= case data_type.to_sym
189
+ when :array
190
+ []
191
+ when :hash
192
+ {}
193
+ when :value
194
+ LocalValue.new
195
+ when :lock
196
+ LocalLock.new(full_name, {:expiration => 60, :timeout => 0.1}.merge(args))
197
+ when :stamped
198
+ Stamped.new(full_name.sub("#{key}_", '').to_sym, get_local_storage(:value, full_name), self)
199
+ else
200
+ raise TypeError.new("Unsupported caching data type encountered: #{data_type}")
201
+ end
202
+ end
203
+
204
+ # Execute block within internal lock
205
+ #
206
+ # @return [Object] result of yield
207
+ # @note for internal use
208
+ def internal_lock
209
+ get_storage(self.class.type, :lock, :internal_access, :timeout => 20, :expiration => 120).lock do
210
+ yield
211
+ end
212
+ end
213
+
214
+ # Fetch data
215
+ #
216
+ # @param name [String, Symbol]
217
+ # @return [Object, NilClass]
218
+ def [](name)
219
+ if(kind = registry[name.to_s])
220
+ get_storage(self.class.type, kind, name)
221
+ else
222
+ nil
223
+ end
224
+ end
225
+
226
+ # Set data
227
+ #
228
+ # @param key [Object]
229
+ # @param val [Object]
230
+ # @note this will never work, thus you should never use it
231
+ def []=(key, val)
232
+ raise 'Setting backend data is not allowed'
233
+ end
234
+
235
+ # Check if cache time has expired
236
+ #
237
+ # @param key [String, Symbol] value key
238
+ # @param stamp [Time, Integer]
239
+ # @return [TrueClass, FalseClass]
240
+ def time_check_allow?(key, stamp)
241
+ Time.now.to_i - stamp.to_i > apply_limit(key)
242
+ end
243
+
244
+ # Apply time limit for data type
245
+ #
246
+ # @param kind [String, Symbol] data type
247
+ # @param seconds [Integer]
248
+ # return [Integer]
249
+ def apply_limit(kind, seconds=nil)
250
+ @apply_limit ||= {}
251
+ if(seconds)
252
+ @apply_limit[kind.to_sym] = seconds.to_i
253
+ end
254
+ @apply_limit[kind.to_sym].to_i
255
+ end
256
+
257
+ # Perform action within lock
258
+ #
259
+ # @param lock_name [String, Symbol] name of lock
260
+ # @param raise_on_locked [TrueClass, FalseClass] raise execption if lock wait times out
261
+ # @return [Object] result of yield
262
+ def locked_action(lock_name, raise_on_locked=false)
263
+ begin
264
+ self[lock_name].lock do
265
+ yield
266
+ end
267
+ rescue => e
268
+ if(e.class.to_s == 'Redis::Lock::LockTimeout')
269
+ raise if raise_on_locked
270
+ else
271
+ raise
272
+ end
273
+ end
274
+ end
275
+
276
+ # Simple value for memory cache
277
+ class LocalValue
278
+ # @return [Object] value
279
+ attr_accessor :value
280
+ def initialize(*args)
281
+ @value = nil
282
+ end
283
+ end
284
+
285
+ # Simple lock for memory cache
286
+ class LocalLock
287
+
288
+ # @return [Symbol] key name
289
+ attr_reader :_key
290
+ # @return [Numeric] timeout
291
+ attr_reader :_timeout
292
+ # @return [Mutex] underlying lock
293
+ attr_reader :_lock
294
+
295
+ # Create new instance
296
+ #
297
+ # @param name [Symbol] name of lock
298
+ # @param args [Hash]
299
+ # @option args [Numeric] :timeout
300
+ def initialize(name, args={})
301
+ @_key = name
302
+ @_timeout = args.fetch(:timeout, -1).to_f
303
+ @_lock = Mutex.new
304
+ end
305
+
306
+ # Aquire lock and yield
307
+ #
308
+ # @yield block to execute within lock
309
+ # @return [Object] result of yield
310
+ def lock
311
+ locked = false
312
+ attempt_start = Time.now.to_f
313
+ while(!locked && (_timeout < 0 || Time.now.to_f - attempt_start < _timeout))
314
+ locked = _lock.try_lock
315
+ end
316
+ if(locked)
317
+ begin
318
+ yield
319
+ ensure
320
+ _lock.unlock if _lock.locked?
321
+ end
322
+ else
323
+ raise Redis::Lock::LockTimeout.new "Timeout on lock #{_key} exceeded #{_timeout} sec"
324
+ end
325
+ end
326
+
327
+ # Clear the lock
328
+ #
329
+ # @note this is a noop
330
+ def clear
331
+ # noop
332
+ end
333
+ end
334
+
335
+ # Wrapper to auto stamp values
336
+ class Stamped
337
+
338
+ # Create new instance
339
+ #
340
+ # @param name [String, Symbol]
341
+ # @param base [Redis::Value, LocalValue]
342
+ # @param cache [Cache]
343
+ def initialize(name, base, cache)
344
+ @name = name.to_sym
345
+ @base = base
346
+ @cache = cache
347
+ end
348
+
349
+ # @return [Object] value stored
350
+ def value
351
+ @base.value[:value] if set?
352
+ end
353
+
354
+ # Store value and update timestamp
355
+ #
356
+ # @param v [Object] value
357
+ # @return [Object]
358
+ def value=(v)
359
+ @base.value = {:stamp => Time.now.to_f, :value => v}
360
+ v
361
+ end
362
+
363
+ # @return [TrueClass, FalseClass] is value set
364
+ def set?
365
+ @base.value.is_a?(Hash)
366
+ end
367
+
368
+ # @return [Float] timestamp of last set (or 0.0 if unset)
369
+ def stamp
370
+ set? ? @base.value[:stamp] : 0.0
371
+ end
372
+
373
+ # Force a timestamp update
374
+ def restamp!
375
+ self.value = value
376
+ end
377
+
378
+ # @return [TrueClass, FalseClass] update is allowed based on stamp and limits
379
+ def update_allowed?
380
+ !set? || @cache.time_check_allow?(@name, @base.value[:stamp])
381
+ end
382
+ end
383
+
384
+ end
385
+ end