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,29 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module Utils
5
+
6
+ # Helper methods for SSH interactions
7
+ module Ssher
8
+
9
+ # Retrieve file from remote node
10
+ #
11
+ # @param address [String]
12
+ # @param user [String]
13
+ # @param path [String] remote file path
14
+ # @param ssh_opts [Hash]
15
+ # @return [String, NilClass]
16
+ def remote_file_contents(address, user, path, ssh_opts={})
17
+ if(path.to_s.strip.empty?)
18
+ raise ArgumentError.new 'No file path provided!'
19
+ end
20
+ require 'net/ssh'
21
+ content = ''
22
+ ssh_session = Net::SSH.start(address, user, ssh_opts)
23
+ content = ssh_session.exec!("sudo cat #{path}")
24
+ content.empty? ? nil : content
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,275 @@
1
+ begin
2
+ require 'chef'
3
+ rescue LoadError
4
+ $stderr.puts "WARN: Failed to load Chef. Chef specific features will be disabled!"
5
+ end
6
+ require 'sfn'
7
+
8
+ module Sfn
9
+ module Utils
10
+
11
+ # Stack serialization helper
12
+ class StackExporter
13
+
14
+ include Bogo::AnimalStrings
15
+ include Sfn::Utils::JSON
16
+
17
+ # default chef environment name
18
+ DEFAULT_CHEF_ENVIRONMENT = '_default'
19
+ # default instance options
20
+ DEFAULT_OPTIONS = Mash.new(
21
+ :chef_popsicle => true,
22
+ :ignored_parameters => ['Environment', 'StackCreator', 'Creator'],
23
+ :chef_environment_parameter => 'Environment'
24
+ )
25
+ # default structure of export payload
26
+ DEFAULT_EXPORT_STRUCTURE = {
27
+ :stack => Mash.new(
28
+ :template => nil,
29
+ :options => {
30
+ :parameters => Mash.new,
31
+ :capabilities => [],
32
+ :notification_topics => []
33
+ }
34
+ ),
35
+ :generator => {
36
+ :timestamp => Time.now.to_i,
37
+ :name => 'SparkleFormation',
38
+ :version => Sfn::VERSION.version,
39
+ :provider => nil
40
+ }
41
+ }
42
+
43
+ # @return [Miasma::Models::Orchestration::Stack]
44
+ attr_reader :stack
45
+ # @return [Hash]
46
+ attr_reader :options
47
+ # @return [Hash]
48
+ attr_reader :stack_export
49
+
50
+ # Create new instance
51
+ #
52
+ # @param stack [Miasma::Models::Orchestration::Stack]
53
+ # @param options [Hash]
54
+ # @option options [KnifeCloudformation::Provider] :provider
55
+ # @option options [TrueClass, FalseClass] :chef_popsicle freeze run list
56
+ # @option options [Array<String>] :ignored_parameters
57
+ # @option options [String] :chef_environment_parameter
58
+ def initialize(stack, options={})
59
+ @stack = stack
60
+ @options = DEFAULT_OPTIONS.merge(options)
61
+ @stack_export = Smash.new
62
+ end
63
+
64
+ # Export stack
65
+ #
66
+ # @return [Hash] exported stack
67
+ def export
68
+ @stack_export = Smash.new(DEFAULT_EXPORT_STRUCTURE).tap do |stack_export|
69
+ [:parameters, :capabilities, :notification_topics].each do |key|
70
+ if(val = stack.send(key))
71
+ stack_export[:stack][key] = val
72
+ end
73
+ end
74
+ stack_export[:stack][:template] = stack.template
75
+ stack_export[:generator][:timestamp] = Time.now.to_i
76
+ stack_export[:generator][:provider] = stack.provider.connection.provider
77
+ if(chef_popsicle? && defined?(Chef))
78
+ freeze_runlists(stack_export)
79
+ end
80
+ remove_ignored_parameters(stack_export)
81
+ stack_export[:stack][:template] = _to_json(
82
+ stack_export[:stack][:template]
83
+ )
84
+ end
85
+ end
86
+
87
+ # Provide query methods on options hash
88
+ #
89
+ # @param args [Object] argument list
90
+ # @return [Object]
91
+ def method_missing(*args)
92
+ m = args.first.to_s
93
+ if(m.end_with?('?') && options.has_key?(k = m.sub('?', '').to_sym))
94
+ !!options[k]
95
+ else
96
+ super
97
+ end
98
+ end
99
+
100
+ protected
101
+
102
+ # Remove parameter values from export that are configured to be
103
+ # ignored
104
+ #
105
+ # @param export [Hash] stack export
106
+ # @return [Hash]
107
+ def remove_ignored_parameters(export)
108
+ options[:ignored_parameters].each do |param|
109
+ if(export[:stack][:options][:parameters])
110
+ export[:stack][:options][:parameters].delete(param)
111
+ end
112
+ end
113
+ export
114
+ end
115
+
116
+ # Environment name to use when interacting with Chef
117
+ #
118
+ # @param export [Hash] current export state
119
+ # @return [String] environment name
120
+ def chef_environment_name(export)
121
+ if(chef_environment_parameter?)
122
+ name = export[:stack][:options][:parameters][options[:chef_environment_parameter]]
123
+ end
124
+ name || DEFAULT_CHEF_ENVIRONMENT
125
+ end
126
+
127
+ # @return [Chef::Environment]
128
+ def environment
129
+ unless(@env)
130
+ @env = Chef::Environment.load('_default')
131
+ end
132
+ @env
133
+ end
134
+
135
+ # Find latest available cookbook version within
136
+ # the configured environment
137
+ #
138
+ # @param cookbook [String] name of cookbook
139
+ # @return [Chef::Version]
140
+ def allowed_cookbook_version(cookbook)
141
+ restriction = environment.cookbook_versions[cookbook]
142
+ requirement = Gem::Requirement.new(restriction)
143
+ Chef::CookbookVersion.available_versions(cookbook).detect do |v|
144
+ requirement.satisfied_by?(Gem::Version.new(v))
145
+ end
146
+ end
147
+
148
+ # Extract the runlist item. Fully expands roles and provides
149
+ # version pegged runlist.
150
+ #
151
+ # @param item [Chef::RunList::RunListItem, Array<String>]
152
+ # @return [Hash] new chef configuration hash
153
+ # @note this will expand all roles
154
+ def extract_runlist_item(item)
155
+ rl_item = item.is_a?(Chef::RunList::RunListItem) ? item : Chef::RunList::RunListItem.new(item)
156
+ static_content = Mash.new(:run_list => [])
157
+ if(rl_item.recipe?)
158
+ cookbook, recipe = rl_item.name.split('::')
159
+ peg_version = allowed_cookbook_version(cookbook)
160
+ static_content[:run_list] << "recipe[#{[cookbook, recipe || 'default'].join('::')}@#{peg_version}]"
161
+ elsif(rl_item.role?)
162
+ role = Chef::Role.load(rl_item.name)
163
+ role.run_list.each do |item|
164
+ static_content = Chef::Mixin::DeepMerge.merge(static_content, extract_runlist_item(item))
165
+ end
166
+ static_content = Chef::Mixin::DeepMerge.merge(
167
+ static_content, Chef::Mixin::DeepMerge.merge(role.default_attributes, role.override_attributes)
168
+ )
169
+ else
170
+ raise TypeError.new("Unknown chef run list item encountered: #{rl_item.inspect}")
171
+ end
172
+ static_content
173
+ end
174
+
175
+ # Expand any detected chef run lists and freeze them within the
176
+ # stack template
177
+ #
178
+ # @param first_run [Hash] chef first run hash
179
+ # @return [Hash]
180
+ def unpack_and_freeze_runlist(first_run)
181
+ extracted_runlists = first_run['run_list'].map do |item|
182
+ extract_runlist_item(cf_replace(item))
183
+ end
184
+ first_run.delete('run_list')
185
+ first_run.replace(
186
+ extracted_runlists.inject(first_run) do |memo, first_run_item|
187
+ Chef::Mixin::DeepMerge.merge(memo, first_run_item)
188
+ end
189
+ )
190
+ end
191
+
192
+ # Freeze chef run lists
193
+ #
194
+ # @param exported [Hash] stack export
195
+ # @return [Hash]
196
+ def freeze_runlists(exported)
197
+ first_runs = locate_runlists(exported)
198
+ first_runs.each do |first_run|
199
+ unpack_and_freeze_runlist(first_run)
200
+ end
201
+ exported
202
+ end
203
+
204
+ # Locate chef run lists within data collection
205
+ #
206
+ # @param thing [Enumerable] collection from export
207
+ # @return [Enumerable] updated collection from export
208
+ def locate_runlists(thing)
209
+ result = []
210
+ case thing
211
+ when Hash
212
+ if(thing['content'] && thing['content']['run_list'])
213
+ result << thing['content']
214
+ else
215
+ thing.each do |k,v|
216
+ result += locate_runlists(v)
217
+ end
218
+ end
219
+ when Array
220
+ thing.each do |v|
221
+ result += locate_runlists(v)
222
+ end
223
+ end
224
+ result
225
+ end
226
+
227
+ # Apply cloudformation function to data
228
+ #
229
+ # @param hsh [Object] stack template item
230
+ # @return [Object]
231
+ def cf_replace(hsh)
232
+ if(hsh.is_a?(Hash))
233
+ case hsh.keys.first
234
+ when 'Fn::Join'
235
+ cf_join(*hsh.values.first)
236
+ when 'Ref'
237
+ cf_ref(hsh.values.first)
238
+ else
239
+ hsh
240
+ end
241
+ else
242
+ hsh
243
+ end
244
+ end
245
+
246
+ # Apply Ref function
247
+ #
248
+ # @param ref_name [Hash]
249
+ # @return [Object] value in parameters
250
+ def cf_ref(ref_name)
251
+ if(stack.parameters.has_key?(ref_name))
252
+ stack.parameters[ref_name]
253
+ else
254
+ raise KeyError.new("No parameter found with given reference name (#{ref_name}). " <<
255
+ "Only parameter based references supported!")
256
+ end
257
+ end
258
+
259
+ # Apply Join function
260
+ #
261
+ # @param delim [String] join delimiter
262
+ # @param args [String, Hash] items to join
263
+ # @return [String]
264
+ def cf_join(delim, args)
265
+ args.map do |arg|
266
+ if(arg.is_a?(Hash))
267
+ cf_replace(arg)
268
+ else
269
+ arg.to_s
270
+ end
271
+ end.join(delim)
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,37 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module Utils
5
+ # Helper for scrubbing stack parameters
6
+ class StackParameterScrubber
7
+
8
+ class << self
9
+
10
+ # Validate attributes within Parameter blocks
11
+ ALLOWED_PARAMETER_ATTRIBUTES = %w(
12
+ Type Default NoEcho AllowedValues AllowedPattern
13
+ MaxLength MinLength MaxValue MinValue Description
14
+ ConstraintDescription
15
+ )
16
+
17
+ # Clean the parameters of the template
18
+ #
19
+ # @param template [Hash]
20
+ # @return [Hash] template
21
+ def scrub!(template)
22
+ parameters = template['Parameters']
23
+ if(parameters)
24
+ parameters.each do |name, options|
25
+ options.delete_if do |attribute, value|
26
+ !ALLOWED_PARAMETER_ATTRIBUTES.include?(attribute)
27
+ end
28
+ end
29
+ template['Parameters'] = parameters
30
+ end
31
+ template
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,124 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module Utils
5
+
6
+ # Helper utility for validating stack parameters
7
+ class StackParameterValidator
8
+ class << self
9
+
10
+ include Bogo::AnimalStrings
11
+
12
+ # Validate a parameters
13
+ #
14
+ # @param value [Object] value for parameter
15
+ # @param parameter_definition [Hash]
16
+ # @option parameter_definition [Array<String>] 'AllowedValues'
17
+ # @option parameter_definition [String] 'AllowedPattern'
18
+ # @option parameter_definition [String, Integer] 'MaxLength'
19
+ # @option parameter_definition [String, Integer] 'MinLength'
20
+ # @option parameter_definition [String, Integer] 'MaxValue'
21
+ # @option parameter_definition [String, Integer] 'MinValue'
22
+ # @return [TrueClass, Array<String>] true if valid. array of string errors if invalid
23
+ def validate(value, parameter_definition)
24
+ return [[:blank, 'Value cannot be blank']] if value.to_s.strip.empty?
25
+ result = %w(AllowedValues AllowedPattern MaxLength MinLength MaxValue MinValue).map do |key|
26
+ if(parameter_definition[key])
27
+ res = self.send(snake(key), value, parameter_definition)
28
+ res == true ? true : [snake(key), res]
29
+ else
30
+ true
31
+ end
32
+ end
33
+ result.delete_if{|x| x == true}
34
+ result.empty? ? true : result
35
+ end
36
+
37
+ # Parameter is within allowed values
38
+ #
39
+ # @param value [String]
40
+ # @param pdef [Hash] parameter definition
41
+ # @option pdef [Array<String>] 'AllowedValues'
42
+ # @return [TrueClass, String]
43
+ def allowed_values(value, pdef)
44
+ if(pdef['AllowedValues'].include?(value))
45
+ true
46
+ else
47
+ "Not an allowed value: #{pdef['AllowedValues'].join(', ')}"
48
+ end
49
+ end
50
+
51
+ # Parameter matches allowed pattern
52
+ #
53
+ # @param value [String]
54
+ # @param pdef [Hash] parameter definition
55
+ # @option pdef [String] 'AllowedPattern'
56
+ # @return [TrueClass, String]
57
+ def allowed_pattern(value, pdef)
58
+ if(value.match(/#{pdef['AllowedPattern']}/))
59
+ true
60
+ else
61
+ "Not a valid pattern. Must match: #{pdef['AllowedPattern']}"
62
+ end
63
+ end
64
+
65
+ # Parameter length is less than or equal to max length
66
+ #
67
+ # @param value [String, Integer]
68
+ # @param pdef [Hash] parameter definition
69
+ # @option pdef [String] 'MaxLength'
70
+ # @return [TrueClass, String]
71
+ def max_length(value, pdef)
72
+ if(value.length <= pdef['MaxLength'].to_i)
73
+ true
74
+ else
75
+ "Value must not exceed #{pdef['MaxLength']} characters"
76
+ end
77
+ end
78
+
79
+ # Parameter length is greater than or equal to min length
80
+ #
81
+ # @param value [String]
82
+ # @param pdef [Hash] parameter definition
83
+ # @option pdef [String] 'MinLength'
84
+ # @return [TrueClass, String]
85
+ def min_length(value, pdef)
86
+ if(value.length >= pdef['MinLength'].to_i)
87
+ true
88
+ else
89
+ "Value must be at least #{pdef['MinLength']} characters"
90
+ end
91
+ end
92
+
93
+ # Parameter value is less than or equal to max value
94
+ #
95
+ # @param value [String]
96
+ # @param pdef [Hash] parameter definition
97
+ # @option pdef [String] 'MaxValue'
98
+ # @return [TrueClass, String]
99
+ def max_value(value, pdef)
100
+ if(value.to_i <= pdef['MaxValue'].to_i)
101
+ true
102
+ else
103
+ "Value must not be greater than #{pdef['MaxValue']}"
104
+ end
105
+ end
106
+
107
+ # Parameter value is greater than or equal to min value
108
+ #
109
+ # @param value [String]
110
+ # @param pdef [Hash] parameter definition
111
+ # @option pdef [String] 'MinValue'
112
+ # @return [TrueClass, String]
113
+ def min_value(value, pdef)
114
+ if(value.to_i >= pdef['MinValue'].to_i)
115
+ true
116
+ else
117
+ "Value must not be less than #{pdef['MinValue']}"
118
+ end
119
+ end
120
+
121
+ end
122
+ end
123
+ end
124
+ end