sfn 0.0.1 → 0.3.0

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