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
@@ -0,0 +1,224 @@
1
+ require 'logger'
2
+ require 'sfn'
3
+
4
+ module Sfn
5
+ # Remote provider interface
6
+ class Provider
7
+
8
+ include Bogo::AnimalStrings
9
+
10
+ # Minimum number of seconds to wait before re-expanding in
11
+ # progress stack
12
+ STACK_EXPAND_INTERVAL = 45
13
+
14
+ # Default interval for refreshing stack list in cache
15
+ STACK_LIST_INTERVAL = 120
16
+
17
+ # @return [Miasma::Models::Orchestration]
18
+ attr_reader :connection
19
+ # @return [Cache]
20
+ attr_reader :cache
21
+ # @return [Thread, NilClass] stack list updater
22
+ attr_accessor :updater
23
+ # @return [TrueClass, FalseClass] async updates
24
+ attr_reader :async
25
+ # @return [Logger, NilClass] logger in use
26
+ attr_reader :logger
27
+ # @return [Numeric] interval between stack expansions
28
+ attr_reader :stack_expansion_interval
29
+ # @return [Numeric] interval between stack list updates
30
+ attr_reader :stack_list_interval
31
+
32
+ # Create new instance
33
+ #
34
+ # @param args [Hash]
35
+ # @option args [Hash] :miasma miasma connection hash
36
+ # @option args [Cache] :cache
37
+ # @option args [TrueClass, FalseClass] :async fetch stacks async (defaults true)
38
+ # @option args [Logger] :logger use custom logger
39
+ # @option args [Numeric] :stack_expansion_interval interval to wait between stack data expands
40
+ # @option args [Numeric] :stack_list_interval interval to wait between stack list refresh
41
+ def initialize(args={})
42
+ args = args.to_smash
43
+ unless(args[:miasma][:provider])
44
+ best_guess = args[:miasma].keys.group_by do |key|
45
+ key.to_s.split('_').first
46
+ end.sort do |x, y|
47
+ y.size <=> x.size
48
+ end.first
49
+ if(best_guess)
50
+ provider = best_guess.first.to_sym
51
+ else
52
+ raise ArgumentError.new 'Cannot auto determine :provider value for credentials'
53
+ end
54
+ else
55
+ provider = args[:miasma].delete(:provider).to_sym
56
+ end
57
+ if(provider == :aws)
58
+ if(args[:miasma][:region])
59
+ args[:miasma][:aws_region] = args[:miasma].delete(:region)
60
+ end
61
+ end
62
+ if(ENV['DEBUG'].to_s.downcase == 'true')
63
+ log_to = STDOUT
64
+ else
65
+ if(Gem.win_platform?)
66
+ log_to = 'NUL'
67
+ else
68
+ log_to = '/dev/null'
69
+ end
70
+ end
71
+ @logger = args.fetch(:logger, Logger.new(log_to))
72
+ @stack_expansion_interval = args.fetch(:stack_expansion_interval, STACK_EXPAND_INTERVAL)
73
+ @stack_list_interval = args.fetch(:stack_list_interval, STACK_LIST_INTERVAL)
74
+ @connection = Miasma.api(
75
+ :provider => provider,
76
+ :type => :orchestration,
77
+ :credentials => args[:miasma]
78
+ )
79
+ @cache = args.fetch(:cache, Cache.new(:local))
80
+ @async = args.fetch(:async, true)
81
+ @miamsa_args = args[:miasma].dup
82
+ cache.init(:stacks_lock, :lock, :timeout => 0.1)
83
+ cache.init(:stacks, :stamped)
84
+ cache.init(:stack_expansion_lock, :lock, :timeout => 0.1)
85
+ if(args.fetch(:fetch, false))
86
+ async ? update_stack_list! : fetch_stacks
87
+ end
88
+ end
89
+
90
+ # @return [Miasma::Orchestration::Stacks]
91
+ def stacks
92
+ connection.stacks.from_json(cached_stacks)
93
+ end
94
+
95
+ # @return [String] json representation of cached stacks
96
+ def cached_stacks
97
+ fetch_stacks unless @initial_fetch_complete
98
+ value = cache[:stacks].value
99
+ value ? MultiJson.dump(MultiJson.load(value).values) : '[]'
100
+ end
101
+
102
+ # @return [Miasma::Orchestration::Stack, NilClass]
103
+ def stack(stack_id)
104
+ stacks.get(stack_id)
105
+ end
106
+
107
+ # Store stack attribute changes
108
+ #
109
+ # @param stack_id [String]
110
+ # @param stack_attributes [Hash]
111
+ # @return [TrueClass]
112
+ def save_expanded_stack(stack_id, stack_attributes)
113
+ current_stacks = MultiJson.load(cached_stacks)
114
+ cache.locked_action(:stacks_lock) do
115
+ logger.info "Saving expanded stack attributes in cache (#{stack_id})"
116
+ current_stacks[stack_id] = stack_attributes.merge('Cached' => Time.now.to_i)
117
+ cache[:stacks].value = MultiJson.dump(current_stacks)
118
+ end
119
+ true
120
+ end
121
+
122
+ # Remove stack from the cache
123
+ #
124
+ # @param stack_id [String]
125
+ # @return [TrueClass, FalseClass]
126
+ def remove_stack(stack_id)
127
+ current_stacks = MultiJson.load(cached_stacks)
128
+ logger.info "Attempting to remove stack from internal cache (#{stack_id})"
129
+ cache.locked_action(:stacks_lock) do
130
+ val = current_stacks.delete(stack_id)
131
+ logger.info "Successfully removed stack from internal cache (#{stack_id})"
132
+ cache[:stacks].value = MultiJson.dump(current_stacks)
133
+ !!val
134
+ end
135
+ end
136
+
137
+ # Expand all lazy loaded attributes within stack
138
+ #
139
+ # @param stack [Miasma::Models::Orchestration::Stack]
140
+ def expand_stack(stack)
141
+ logger.info "Stack expansion requested (#{stack.id})"
142
+ if((stack.in_progress? && Time.now.to_i - stack.attributes['Cached'].to_i > stack_expansion_interval) ||
143
+ !stack.attributes['Cached'])
144
+ begin
145
+ expanded = false
146
+ cache.locked_action(:stack_expansion_lock) do
147
+ expanded = true
148
+ stack.reload
149
+ stack.data['Cached'] = Time.now.to_i
150
+ end
151
+ if(expanded)
152
+ save_expanded_stack(stack.id, stack.to_json)
153
+ end
154
+ rescue => e
155
+ logger.error "Stack expansion failed (#{stack.id}) - #{e.class}: #{e}"
156
+ end
157
+ else
158
+ logger.info "Stack has been cached within expand interval. Expansion prevented. (#{stack.id})"
159
+ end
160
+ end
161
+
162
+ # Request stack information and store in cache
163
+ #
164
+ # @return [TrueClass]
165
+ def fetch_stacks
166
+ cache.locked_action(:stacks_lock) do
167
+ logger.info "Lock aquired for stack update. Requesting stacks from upstream. (#{Thread.current})"
168
+ stacks = Hash[
169
+ connection.stacks.reload.all.map do |stack|
170
+ [stack.id, stack.attributes]
171
+ end
172
+ ]
173
+ if(cache[:stacks].value)
174
+ existing_stacks = MultiJson.load(cache[:stacks].value)
175
+ # Force common types
176
+ stacks = MultiJson.load(MultiJson.dump(stacks))
177
+ # Remove stacks that have been deleted
178
+ stale_ids = existing_stacks.keys - stacks.keys
179
+ stacks = existing_stacks.to_smash.deep_merge(stacks)
180
+ stale_ids.each do |stale_id|
181
+ stacks.delete(stale_id)
182
+ end
183
+ end
184
+ cache[:stacks].value = stacks.to_json
185
+ logger.info 'Stack list has been updated from upstream and cached locally'
186
+ end
187
+ @initial_fetch_complete = true
188
+ end
189
+
190
+ # Start async stack list update. Creates thread that loops every
191
+ # `self.stack_list_interval` seconds and refreshes stack list in cache
192
+ #
193
+ # @return [TrueClass, FalseClass]
194
+ def update_stack_list!
195
+ if(updater.nil? || !updater.alive?)
196
+ self.updater = Thread.new{
197
+ loop do
198
+ begin
199
+ fetch_stacks
200
+ sleep(stack_list_interval)
201
+ rescue => e
202
+ logger.error "Failure encountered on stack fetch: #{e.class} - #{e}"
203
+ end
204
+ end
205
+ }
206
+ true
207
+ else
208
+ false
209
+ end
210
+ end
211
+
212
+ # Build API connection for service type
213
+ #
214
+ # @param service [String, Symbol]
215
+ # @return [Miasma::Model]
216
+ def service_for(service)
217
+ connection.api_for(service)
218
+ end
219
+
220
+ end
221
+ end
222
+
223
+ # Release the monkeys!
224
+ Sfn::MonkeyPatch::Stack
data/lib/sfn/utils.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ # Utility classes and modules
5
+ module Utils
6
+
7
+ autoload :Output, 'sfn/utils/output'
8
+ autoload :StackParameterValidator, 'sfn/utils/stack_parameter_validator'
9
+ autoload :StackParameterScrubber, 'sfn/utils/stack_parameter_scrubber'
10
+ autoload :StackExporter, 'sfn/utils/stack_exporter'
11
+ autoload :Debug, 'sfn/utils/debug'
12
+ autoload :JSON, 'sfn/utils/json'
13
+ autoload :Ssher, 'sfn/utils/ssher'
14
+ autoload :ObjectStorage, 'sfn/utils/object_storage'
15
+ autoload :PathSelector, 'sfn/utils/path_selector'
16
+
17
+ # Provide methods directly from module for previous version compatibility
18
+ extend JSON
19
+ extend ObjectStorage
20
+ extend Bogo::AnimalStrings
21
+
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
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 "<SparkleFormation>: #{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,37 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module Utils
5
+
6
+ # JSON helper methods
7
+ module JSON
8
+
9
+ # Convert to JSON
10
+ #
11
+ # @param thing [Object]
12
+ # @return [String]
13
+ def _to_json(thing)
14
+ MultiJson.dump(thing)
15
+ end
16
+
17
+ # Load JSON data
18
+ #
19
+ # @param thing [String]
20
+ # @return [Object]
21
+ def _from_json(thing)
22
+ MultiJson.load(thing)
23
+ end
24
+
25
+ # Format object into pretty JSON
26
+ #
27
+ # @param thing [Object]
28
+ # @return [String]
29
+ def _format_json(thing)
30
+ thing = _from_json(thing) if thing.is_a?(String)
31
+ MultiJson.dump(thing, :pretty => true)
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
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 'sfn'
2
+
3
+ module Sfn
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 'sfn'
2
+
3
+ module Sfn
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