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